/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";
type int = number;
type VisitorKeyed = {[index:string] : (node: Node1)=>void};
type Origin = LexerToken | ExternalOrigin;
type OverLoadSuccess = {failure: undefined, func: Func, unificationContext: UnificationContext, typeArguments: Node1[]};
type OverLoadFailure = {failures: OverloadResolutionFailure[], func: undefined};
type OverLoad = OverLoadSuccess|OverLoadFailure;
type InferTypeSuccess = OverLoadSuccess;
type InferTypesFail = {failure: OverloadResolutionFailure, func: undefined};
type InferTypeRet = InferTypeSuccess | InferTypesFail;
type ValueType = Node1 | number | EPtr;
type EPtrBoxValue =  Node1 | EArrayRef | boolean | number | EPtr;
type TypeParameters = TypeVariable | ConstexprTypeParameter;
type CallResolve = { call: CallExpression, resultType: Type};
type FuncInstantiatorInstances = {func: Func, typeArguments: Node1[]};
type ImplementationDataType = {type: Type | undefined, fieldName: string, offset: int, structSize: int, fieldSize: int};
const DefaultImplementationData: ImplementationDataType  = {type: undefined, fieldName: "", offset: -1, structSize: -1, fieldSize: -1};
type VerityResultType = {result: boolean, reason: string};
type PrepareToVerityType = () => VerityResultType;
type CheckFailParamType = (origin: string, lineNumberOffset: int, text: string)=>Program;
//declare const print: (message: any)=>void;
declare function print(str:any):string;

interface RegExpType {
    new(pattern: RegExp | string): RegExp;
    new(pattern: string, flags?: string): RegExp;
    (pattern: RegExp | string): RegExp;
    (pattern: string, flags?: string): RegExp;
    readonly prototype: RegExp;
    rightContext: string;
    leftContext: string;

    // Non-standard extensions
    $1: string;
    $2: string;
    $3: string;
    $4: string;
    $5: string;
    $6: string;
    $7: string;
    $8: string;
    $9: string;
    lastMatch: string;
}

class Node1 {
    _origin: Origin;
    _addressSpace: string;
    ePtr: EPtr;
    _name: string;
    program: Program;
    hasBecome: boolean;
    _target: Node1;
    _type: Type;
    _isPtr: boolean;
    _isLiteral: boolean;
    get name() { return this._name; }
    set name(name: string) { this._name = name; }
    implementation: (value: (Node1|EPtr)[], node?: Node1) => EPtr;
    verifyAsArgument(unificationContext: UnificationContext): VerityResultType {
        throw new Error("no verifyAsArgument");
    }

    get type() { return this._type; }
    set type(val: Type) { this._type = val; }

    get addressSpace() {
        return this._addressSpace;
    }

    set addressSpace(val: string) { this._addressSpace = val; }

    verifyAsParameter(unificationContext: UnificationContext): VerityResultType {
        throw new Error("no verifyAsparameter!");
    }

    visit(visitor: Visitor|Rewriter|Instantiate|RValueFinder|TypeParameterRewriter): Node1 | EPtr | string
    {
        let visitFunc: (node: Node1)=>void;

        if (this.hasBecome) {
            visitFunc = (<VisitorKeyed><unknown>visitor)["visitIdentityExpression"];
        } else {
            let name = this.constructor.name;
            if (name == "IntLiteral" || name == "UintLiteral" || name == "FloatLiteral" || name == "DoubleLiteral") {
                name = "GenericLiteral";
            } else if (name == "IntLiteralType" || name == "UintLiteralType" || name == "FloatLiteralType" || name == "DoubleLiteralType") {
                name = "GenericLiteralType";
            }
            visitFunc = (<VisitorKeyed><unknown>visitor)["visit" + name];
        }

        if (!visitFunc)
            throw new Error("No visit function for " + this.constructor.name + " in " + visitor.constructor.name);
        let returnValue = <Node1>visitFunc.call(visitor, this);
        if ("returnValue" in visitor)
            returnValue = <Node1>visitor.returnValue;

        return returnValue;
    }

    static visit(node: Node1 | string, visitor: Visitor|Rewriter)
    {
        if (node instanceof Node1)
            return node.visit(visitor);
        return node;
    }


    unify(unificationContext: UnificationContext, other: Node1)
    {
        if (!other)
            throw new Error("Null other");
        let unifyThis = this.unifyNode;
        let unifyOther = other.unifyNode;
        if (unifyThis == unifyOther)
            return true;
        if (unifyOther.typeVariableUnify(unificationContext, unifyThis))
            return true;
        return unifyThis.unifyImpl(unificationContext, unifyOther);
    }

    unifyImpl(unificationContext: UnificationContext, other: Node1)
    {
        if (other.typeVariableUnify(unificationContext, this))
            return true;
        return this == other;
    }

    typeVariableUnify(unificationContext: UnificationContext, other: Node1)
    {
        return false;
    }

    _typeVariableUnifyImpl(unificationContext: UnificationContext, other: Node1)
    {
        let realThis = unificationContext.find(this);
        if (realThis != this)
            return realThis.unify(unificationContext, other);

        unificationContext.union(this, other);
        return true;
    }

    // Most type variables don't care about this.
    prepareToVerify(unificationContext: UnificationContext): PrepareToVerityType { return undefined; }
    commitUnification(unificationContext: UnificationContext) { }

    get target(): Node1 {
        return this._target;
    }
    get unifyNode(): Node1 {
        if (this.hasBecome) return this.target.unifyNode;
        return this;
    }
    get isConstexpr() { return false; }
    get isLValue() { return false; }
    get isUnifiable() { return false; }
    get isLiteral() { return this._isLiteral == true; }

    get isNative() { return false; }
    get origin() { return this._origin; }
    get isPtr() { return this._isPtr === true; }

    get kind(): object { return <object>Node1; }

    conversionCost(unificationContext: UnificationContext) { return 0; }

    equals(other: Node1)
    {
        let unificationContext = new UnificationContext();
        if (this.unify(unificationContext, other) && unificationContext.verify().result)
            return unificationContext;
        return false;
    }

    equalsWithCommit(other: Node1)
    {
        let unificationContext = this.equals(other);
        if (!unificationContext)
            return false;
        unificationContext.commit();
        return unificationContext;
    }

    commit()
    {
        let unificationContext = new UnificationContext();
        unificationContext.addExtraNode(this);
        let result: VerityResultType = unificationContext.verify();
        if (!result.result)
            throw new Error("Could not infer type: " + result.reason);
            // throw new WError(node.origin.originString, "Could not infer type: " + result.reason);
        unificationContext.commit();
        return unificationContext.find(this);
    }

    substitute(parameters: Node1[], argumentList: Node1[]): Node1
    {
        return <Node1>this.visit(new Substitution(parameters, argumentList));
    }

    substituteToUnification(parameters: Node1[], unificationContext: UnificationContext): Node1
    {
        return this.substitute(
            parameters,
            parameters.map((type: Node1) => unificationContext.find(type)));
    }
}

class Type extends Node1 {
    canRepresent: (value: number) => boolean;
    createLiteral: (origin: Origin, value: number)=>GenericLiteral;
    successorValue: (value: number)=>number;
    valuesEqual: (a: number, b: number)=>boolean;
    formatValueFromIntLiteral: (value: number)=>number;
    formatValueFromUintLiteral: (value: number)=>number;
    allValues: ()=>IterableIterator<{value: number, name: Object}>;
    instantiate: (typeArguments: Node1[]) => Type;
    formatValueFromFloatLiteral: (value: number)=>number;
    formatValueFromDoubleLiteral: (value: number)=>number;
    populateDefaultValue: (buffer: EBuffer, offset: number)=>void;
    _elementType: Type;

    get elementType() { return this._elementType; }

    _isPrimitive: boolean;
    _isInt: boolean;
    _isNumber: boolean;
    _isSigned: boolean;
    _isFloating: boolean;
    _size: int;
    _isArrayRef: boolean;
    defaultValue: number;

    get isPrimitive() { return this._isPrimitive; }
    set isPrimitive(value: boolean) { this._isPrimitive = value; }

    get isInt() { return this._isInt; }
    set isInt(val: boolean) { this._isInt = val; }

    get isNumber() { return this._isNumber; }
    set isNumber(val: boolean) { this._isNumber = val; }

    get isSigned() { return this._isSigned; }
    set isSigned(val: boolean) { this._isSigned = val; }

    get isFloating() { return this._isFloating; }
    set isFloating(val: boolean) { this._isFloating = val; }

    get size() { return this._size; }
    set size(val: int) { this._size = val; }

    get typeParameters(): Node1[] { return []; }
    get kind() { return Type; }
    get isArray() { return false; }
    get isArrayRef() { return this._isArrayRef === true; }
    get isRef() { return this.isPtr || this.isArrayRef; }

    inherits(protocol: ProtocolRef): VerityResultType
    {
        if (!protocol)
            return {result: true, reason: undefined};
        return protocol.hasHeir(this);
    }

    get instantiatedType(): Type { return <Type>this.visit(new InstantiateImmediates()); }

    // Have to call these on the unifyNode.
    argumentForAndOverload(origin: Origin, value: ValueType): Node1
    {
        return new MakePtrExpression(origin, <Node1>value);
    }
    argumentTypeForAndOverload(origin: Origin, type?: Type): Type
    {
        return new PtrType(origin, "thread", this);
    }
    returnTypeFromAndOverload(origin: Origin): Type
    {
        throw new WTypeError(origin.originString, "By-pointer overload returned non-pointer type: " + this);
    }
}

function ReferencePopulate(buffer: EBuffer, offset: number) {
    buffer.set(offset, null);
}

class ReferenceType extends Type {
    constructor(origin: Origin, addressSpace: string, elementType: Type)
    {
        if (!elementType)
            throw new Error("Null elementType");
        if (!origin)
            throw new Error("Null origin");
        super();
        validateAddressSpace(addressSpace);
        this._origin = origin;
        this._addressSpace = addressSpace;
        this._elementType = elementType;
        this.populateDefaultValue = ReferencePopulate;
    }

    get elementType() { return this._elementType; }

    get addressSpace() {
        if (this.hasBecome) return  this.target.addressSpace;
        return this._addressSpace;
    }

    get size() { return 1; }
}

class Value extends Node1 {
    __proto__: Object;
    _name: string;
    get kind() { return Value; }
    get isConstexpr() {
      if (this.hasBecome) return this.target.isConstexpr;
      return false;
    }
    get isLValue() {
        if (this.hasBecome) return this.target.isLValue;
        return false;
    }
    get notLValueReason(): string { return null; }
    get varIsLValue() { return false; }

    get notLValueReasonString()
    {
        let result = this.notLValueReason;
        if (result)
            return "\n" + result;
        return "";
    }

    become(otherValue: Value)
    {
        // NOTE: Make sure that IdentityExpression implements unifyNode and all that
        let origin = this.origin;
        // this.__proto__ = IdentityExpression.prototype;
        this._origin = origin;
        this._target = otherValue;
        this.hasBecome = true;
    }
}

class Expression extends Value {
    constructor(origin: Origin)
    {
        super();
        this._origin = origin;
    }

}

class Rewriter {
    _mapping: Map<Node1, Node1>;
    constructor()
    {
        this._mapping = new Map();
    }

    _mapNode(oldItem: Node1, newItem: Node1)
    {
        this._mapping.set(oldItem, newItem);
        return newItem;
    }

    _getMapping(oldItem: Node1): Node1
    {
        let result = <Node1>this._mapping.get(oldItem);
        if (result)
            return result;
        return oldItem;
    }

    // We return identity for anything that is not inside a function/struct body. When processing
    // function bodies, we only recurse into them and never out of them - for example if there is a
    // function call to another function then we don't rewrite the other function. This is how we stop
    // that.
    visitFuncDef(node: FuncDef) { return node; }
    visitNativeFunc(node: NativeFunc) { return node; }
    visitNativeFuncInstance(node: NativeFuncInstance) { return node; }
    visitNativeType(node: NativeType): Node1 { return node; }
    visitTypeDef(node: TypeDef): TypeDef { return node; }
    visitStructType(node: StructType): Node1 { return node; }
    visitConstexprTypeParameter(node: ConstexprTypeParameter): Node1 { return node; }
    visitProtocolDecl(node: ProtocolDecl) { return node; }
    visitEnumType(node: EnumType) { return node; }

    // This is almost wrong. We instantiate Func in Substitution in ProtocolDecl. Then, we end up
    // not rewriting type variables. I think that just works because not rewriting them there is OK.
    // Everywhere else, it's mandatory that we don't rewrite these because we always assume that
    // type variables are outside the scope of rewriting.
    visitTypeVariable(node: TypeVariable): Node1 { return node; }

    visitProtocolFuncDecl(node: ProtocolFuncDecl)
    {
        let result: ProtocolFuncDecl = new ProtocolFuncDecl(
            node.origin, node.name,
            <Type>node.returnType.visit(this),
            <TypeParameters[]>node.typeParameters.map((parameter: Node1) => parameter.visit(this)),
            <FuncParameter[]>node.parameters.map((parameter: Node1) => parameter.visit(this)),
            node.isCast,
            node.shaderType);
        result.protocolDecl = node.protocolDecl;
        result.possibleOverloads = node.possibleOverloads;
        return result;
    }

    visitNativeTypeInstance(node: NativeTypeInstance)
    {
        return new NativeTypeInstance(
            <Type>node.type.visit(this),
            <Type[]>node.typeArguments.map((argument: Node1) => argument.visit(this)));
    }

    visitFuncParameter(node: FuncParameter): Node1
    {
        let result = new FuncParameter(node.origin, node.name, <TypeRef>node.type.visit(this));
        this._mapNode(node, result);
        result.ePtr = node.ePtr;
        return result;
    }

    visitVariableDecl(node: VariableDecl): Node1
    {
        let result = new VariableDecl(
            node.origin, node.name,
            <Type>node.type.visit(this),
            <Expression>Node1.visit(node.initializer, this));
        this._mapNode(node, result);
        result.ePtr = node.ePtr;
        return result;
    }

    visitBlock(node: Block)
    {
        let result = new Block(node.__origin);
        for (let statement of node.statements)
            result.add(<Node1>statement.visit(this));
        return result;
    }

    visitCommaExpression(node: CommaExpression)
    {
        return new CommaExpression(node.origin, <Expression[]>node.list.map((expression: Node1) => {
            let result = expression.visit(this);
            if (!result)
                throw new Error("Null result from " + expression);
            return result;
        }));
    }

    visitProtocolRef(node: ProtocolRef)
    {
        return node;
    }

    visitTypeRef(node: TypeRef): Node1 | EPtr | string
    {
        let result = new TypeRef(node.origin, node.name, <Node1[]>node.typeArguments.map((typeArgument: Node1) => typeArgument.visit(this)));
        result.type = <Type>Node1.visit(node.type, this);
        return result;
    }

    visitField(node: Field)
    {
        return new Field(node.origin, node.name, <Type>node.type.visit(this));
    }

    visitEnumMember(node: EnumMember)
    {
        return new EnumMember(node.origin, node.name, <Expression>(node.value.visit(this)));
    }

    visitEnumLiteral(node: EnumLiteral)
    {
        let result = new EnumLiteral(node.origin, node.member);
        result.ePtr = node.ePtr;
        return result;
    }

    visitReferenceType(node: ReferenceType)
    {
        if (node instanceof PtrType) {
            return new PtrType(node.origin, node.addressSpace, <Type>node.elementType.visit(this));
        } else if (node instanceof ArrayRefType) {
            return new ArrayRefType(node.origin, node.addressSpace, <Type>node.elementType.visit(this));
        } else if (node instanceof ReferenceType) {
            return new ReferenceType(node.origin, node.addressSpace, <Type>node.elementType.visit(this));
        } else {
            throw new Error("unknow reference type");
        }
    }

    visitPtrType(node: PtrType)
    {
        return this.visitReferenceType(node);
    }

    visitArrayRefType(node: ArrayRefType)
    {
        return this.visitReferenceType(node);
    }

    visitArrayType(node: ArrayType)
    {
        return new ArrayType(node.origin, <Type>node.elementType.visit(this), <GenericLiteral>node.numElements.visit(this));
    }

    visitAssignment(node: Assignment)
    {
        let result = new Assignment(node.origin, <VariableRef>node.lhs.visit(this), <VariableRef>node.rhs.visit(this));
        result.type = <Type>Node1.visit(node.type, this);
        return result;
    }

    visitReadModifyWriteExpression(node: ReadModifyWriteExpression)
    {
        let result = new ReadModifyWriteExpression(node.origin, <PropertyAccessExpression>node.lValue.visit(this));
        result.oldValueVar = <AnonymousVariable>node.oldValueVar.visit(this);
        result.newValueVar = <AnonymousVariable>node.newValueVar.visit(this);
        result.newValueExp = <CommaExpression>node.newValueExp.visit(this);
        result.resultExp = <CommaExpression>node.resultExp.visit(this);
        return result;
    }

    visitDereferenceExpression(node: DereferenceExpression)
    {
        let result = new DereferenceExpression(node.origin, <VariableRef>node.ptr.visit(this));
        result.type = <Type>Node1.visit(node.type, this);
        result.addressSpace = <string>node.addressSpace;
        return result;
    }

    _handlePropertyAccessExpression(result: PropertyAccessExpression, node: PropertyAccessExpression)
    {
        result.possibleGetOverloads = node.possibleGetOverloads;
        result.possibleSetOverloads = node.possibleSetOverloads;
        result.possibleAndOverloads = node.possibleAndOverloads;
        result.baseType = <Type>Node1.visit(node.baseType, this);
        result.callForGet = <CallExpression>Node1.visit(node.callForGet, this);
        result.resultTypeForGet = <TypeRef>Node1.visit(node.resultTypeForGet, this);
        result.callForAnd = <CallExpression>Node1.visit(node.callForAnd, this);
        result.resultTypeForAnd = <TypeRef>Node1.visit(node.resultTypeForAnd, this);
        result.callForSet = <CallExpression>Node1.visit(node.callForSet, this);
        result.errorForSet = node.errorForSet;
        result.updateCalls();
    }

    visitDotExpression(node: DotExpression): Node1
    {
        let result = new DotExpression(node.origin, <Expression>node.struct1.visit(this), node.fieldName);
        this._handlePropertyAccessExpression(result, node);
        return result;
    }

    visitIndexExpression(node: IndexExpression): Node1
    {
        let result = new IndexExpression(node.origin, <Expression>node.array.visit(this), <Expression>node.index.visit(this));
        this._handlePropertyAccessExpression(result, node);
        return result;
    }

    visitMakePtrExpression(node: MakePtrExpression)
    {
        let result = new MakePtrExpression(node.origin, <Node1>node.lValue.visit(this));
        result.ePtr = node.ePtr;
        return result;
    }

    visitMakeArrayRefExpression(node: MakeArrayRefExpression)
    {
        let result = new MakeArrayRefExpression(node.origin, <Value>node.lValue.visit(this));
        if (node.numElements)
            result.numElements = <GenericLiteral>node.numElements.visit(this);
        result.ePtr = node.ePtr;
        return result;
    }

    visitConvertPtrToArrayRefExpression(node: ConvertPtrToArrayRefExpression)
    {
        let result = new ConvertPtrToArrayRefExpression(node.origin, <Node1>node.lValue.visit(this));
        result.ePtr = node.ePtr;
        return result;
    }

    visitVariableRef(node: VariableRef): Node1
    {
        let result = new VariableRef(node.origin, node.name);
        result.variable = <Value>this._getMapping(node.variable);
        return result;
    }

    visitReturn(node: Return)
    {
        return new Return(node.origin, <Value>Node1.visit(node.value, this));
    }

    visitContinue(node: Continue)
    {
        return new Continue(node.origin);
    }

    visitBreak(node: Break)
    {
        return new Break(node.origin);
    }

    visitTrapStatement(node: TrapStatement)
    {
        return new TrapStatement(node.origin);
    }

    visitGenericLiteral(node: GenericLiteral)
    {
        // FIXME: This doesn't seem right.
        let result = new IntLiteral(node.origin, node.value);
        result.type = <Type>node.type.visit(this);
        result.ePtr = node.ePtr;
        return result;
    }

    visitGenericLiteralType(node: GenericLiteralType)
    {
        let result: GenericLiteralType;
        if (node instanceof DoubleLiteralType) {
            result = new DoubleLiteralType(node.origin, node.value);
        }
        else if (node instanceof FloatLiteralType) {
            result = new FloatLiteralType(node.origin, node.value);
        }
        else if (node instanceof IntLiteralType) {
            result = new IntLiteralType(node.origin, node.value);
        }
        else if (node instanceof UintLiteralType) {
            result = new UintLiteralType(node.origin, node.value);
        }
        else {
            throw new Error("unknow genericLiteralType: " + node.constructor.name);
        }

        result.type = <Type>Node1.visit(node.type, this);
        result.preferredType = <TypeRef>node.preferredType.visit(this);
        return result;
    }

    visitBoolLiteral(node: BoolLiteral)
    {
        return node;
    }

    visitNullLiteral(node: NullLiteral)
    {
        let result = new NullLiteral(node.origin);
        result.type = <Type>node.type.visit(this);
        result.ePtr = node.ePtr;
        return result;
    }

    visitNullType(node: NullType)
    {
        let result = new NullType(node.origin);
        result.type = <Type>Node1.visit(node.type, this);
        return result;
    }

    processDerivedCallData(node: CallExpression, result: CallExpression)
    {
        let handleTypeArguments = (actualTypeArguments: Type[]): Type[] => {
            if (actualTypeArguments)
                return <Type[]>actualTypeArguments.map((actualTypeArgument: Node1) => actualTypeArgument.visit(this));
            else
                return null;
        }
        result.actualTypeArguments = handleTypeArguments(node.actualTypeArguments);
        result.instantiatedActualTypeArguments = handleTypeArguments(node.instantiatedActualTypeArguments);
        let argumentTypes = node.argumentTypes;
        if (argumentTypes)
            result.argumentTypes = <Type[]>argumentTypes.map(argumentType => argumentType.visit(this));
        result.func = node.func;
        result.nativeFuncInstance = node.nativeFuncInstance;
        result.possibleOverloads = node.possibleOverloads;
        if (node.isCast)
            result.setCastData(<TypeRef>node.returnType.visit(this));
        result.resultType = <TypeRef>Node1.visit(node.resultType, this);
        result.resultEPtr = node.resultEPtr;
        return result;
    }

    visitCallExpression(node: CallExpression): Node1
    {
        let result = new CallExpression(
            node.origin, node.name,
            <Type[]>node.typeArguments.map(typeArgument => typeArgument.visit(this)),
            <Node1[]>node.argumentList.map(argument => Node1.visit(argument, this)));
        return this.processDerivedCallData(node, result);
    }

    visitFunctionLikeBlock(node: FunctionLikeBlock)
    {
        let result = new FunctionLikeBlock(
            node.origin,
            <Type>Node1.visit(node.returnType, this),
            <Node1[]>node.argumentList.map((argument: Node1) => argument.visit(this)),
            <FuncParameter[]>node.parameters.map((parameter: Node1) => parameter.visit(this)),
            <Node1>node.body.visit(this));
        result.returnEPtr = node.returnEPtr;
        return result;
    }

    visitLogicalNot(node: LogicalNot)
    {
        let result = new LogicalNot(node.origin, <CallExpression>node.operand.visit(this));
        result.ePtr = node.ePtr;
        return result;
    }

    visitLogicalExpression(node: LogicalExpression)
    {
        let result = new LogicalExpression(node.origin, node.text, <CallExpression>node.left.visit(this), <CallExpression>node.right.visit(this));
        result.ePtr = node.ePtr;
        return result;
    }

    visitIfStatement(node: IfStatement)
    {
        return new IfStatement(node.origin, <CallExpression>node.conditional.visit(this), <Node1>node.body.visit(this), <Node1>Node1.visit(node.elseBody, this));
    }

    visitWhileLoop(node: WhileLoop)
    {
        return new WhileLoop(node.origin, <CallExpression>node.conditional.visit(this), <Node1>node.body.visit(this));
    }

    visitDoWhileLoop(node: DoWhileLoop)
    {
        return new DoWhileLoop(node.origin, <Node1>node.body.visit(this), <CallExpression>node.conditional.visit(this));
    }

    visitForLoop(node: ForLoop)
    {
        return new ForLoop(node.origin,
            <Expression>Node1.visit(node.initialization, this),
            <CallExpression>Node1.visit(node.condition, this),
            <Expression>Node1.visit(node.increment, this),
            <Node1>node.body.visit(this));
    }

    visitSwitchStatement(node: SwitchStatement)
    {
        let result = new SwitchStatement(node.origin, <Value>Node1.visit(node.value, this));
        for (let switchCase of node.switchCases)
            result.add(<SwitchCase>switchCase.visit(this));
        result.type = <Type>Node1.visit(node.type, this);
        return result;
    }

    visitSwitchCase(node: SwitchCase)
    {
        return new SwitchCase(node.origin, <Value>Node1.visit(node.value, this), <Block>node.body.visit(this));
    }

    visitAnonymousVariable(node: AnonymousVariable)
    {
        let result = new AnonymousVariable(node.origin, <Type>node.type.visit(this));
        result.index = node.index; // XTS ._index = ._index
        this._mapNode(node, result);
        result.ePtr = node.ePtr;
        return result;
    }

    visitIdentityExpression(node: Node1)
    {
        return new IdentityExpression(<Expression>node.target.visit(this));
    }
}

/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";

class Visitor {
    returnValue: Node1;
    visitProgram(node: Program)
    {
        for (let statement of node.topLevelStatements)
            statement.visit(this);
    }

    visitFunc(node: Func)
    {
        node.returnType.visit(this);
        for (let typeParameter of node.typeParameters)
            typeParameter.visit(this);
        for (let parameter of node.parameters)
            parameter.visit(this);
    }

    visitProtocolFuncDecl(node: ProtocolFuncDecl)
    {
        this.visitFunc(node);
    }

    visitFuncParameter(node: FuncParameter)
    {
        node.type.visit(this);
    }

    visitFuncDef(node: FuncDef)
    {
        this.visitFunc(node);
        node.body.visit(this);
    }

    visitNativeFunc(node: NativeFunc)
    {
        this.visitFunc(node);
    }

    visitNativeFuncInstance(node: NativeFuncInstance)
    {
        this.visitFunc(node);
        node.func.visitImplementationData(node.implementationData, this);
    }

    visitBlock(node: Block)
    {
        for (let statement of node.statements)
            statement.visit(this);
    }

    visitCommaExpression(node: CommaExpression)
    {
        for (let expression of node.list)
            (<Node1>expression).visit(this);
    }

    visitProtocolRef(node: ProtocolRef)
    {
    }

    visitProtocolDecl(node: ProtocolDecl)
    {
        for (let protocol of node.extends)
            protocol.visit(this);
        for (let signature of node.signatures)
            signature.visit(this);
    }

    visitTypeRef(node: TypeRef)
    {
        for (let typeArgument of node.typeArguments)
            typeArgument.visit(this);
    }

    visitNativeType(node: NativeType)
    {
        for (let typeParameter of node.typeParameters)
            typeParameter.visit(this);
    }

    visitNativeTypeInstance(node: NativeTypeInstance)
    {
        node.type.visit(this);
        for (let typeArgument of node.typeArguments)
            typeArgument.visit(this);
    }

    visitTypeDef(node: TypeDef)
    {
        for (let typeParameter of node.typeParameters)
            typeParameter.visit(this);
        node.type.visit(this);
    }

    visitStructType(node: StructType): undefined
    {
        for (let typeParameter of node.typeParameters)
            typeParameter.visit(this);
        for (let field of node.fields)
            field.visit(this);
        return undefined;
    }

    visitTypeVariable(node: TypeVariable)
    {
        Node1.visit(node.protocol, this);
    }

    visitConstexprTypeParameter(node: ConstexprTypeParameter)
    {
        node.type.visit(this);
    }

    visitField(node: Field)
    {
        node.type.visit(this);
    }

    visitEnumType(node: EnumType)
    {
        node.baseType.visit(this);
        for (let member of node.members)
            member.visit(this);
    }

    visitEnumMember(node: EnumMember)
    {
        Node1.visit(<Node1>node.value, this);
    }

    visitEnumLiteral(node: EnumLiteral)
    {
    }

    visitElementalType(node: ReferenceType|ArrayType)
    {
        node.elementType.visit(this);
    }

    visitReferenceType(node: ReferenceType)
    {
        this.visitElementalType(node);
    }

    visitPtrType(node: PtrType)
    {
        this.visitReferenceType(node);
    }

    visitArrayRefType(node: ArrayRefType)
    {
        this.visitReferenceType(node);
    }

    visitArrayType(node: ArrayType)
    {
        this.visitElementalType(node);
        node.numElements.visit(this);
    }

    visitVariableDecl(node: VariableDecl)
    {
        node.type.visit(this);
        Node1.visit(node.initializer, this);
    }

    visitAssignment(node: Assignment)
    {
        node.lhs.visit(this);
        node.rhs.visit(this);
        Node1.visit(node.type, this);
    }

    visitReadModifyWriteExpression(node: ReadModifyWriteExpression)
    {
        node.lValue.visit(this);
        node.oldValueVar.visit(this);
        node.newValueVar.visit(this);
        node.newValueExp.visit(this);
        node.resultExp.visit(this);
    }

    visitDereferenceExpression(node: DereferenceExpression)
    {
        node.ptr.visit(this);
    }

    _handlePropertyAccessExpression(node: PropertyAccessExpression)
    {
        Node1.visit(node.baseType, this);
        Node1.visit(node.callForGet, this);
        Node1.visit(node.resultTypeForGet, this);
        Node1.visit(node.callForAnd, this);
        Node1.visit(node.resultTypeForAnd, this);
        Node1.visit(node.callForSet, this);
    }

    visitDotExpression(node: DotExpression)
    {
        node.struct1.visit(this);
        this._handlePropertyAccessExpression(node);
    }

    visitIndexExpression(node: IndexExpression)
    {
        node.array.visit(this);
        node.index.visit(this);
        this._handlePropertyAccessExpression(node);
    }

    visitMakePtrExpression(node: MakePtrExpression)
    {
        node.lValue.visit(this);
    }

    visitMakeArrayRefExpression(node: MakeArrayRefExpression)
    {
        node.lValue.visit(this);
        Node1.visit(node.numElements, this);
    }

    visitConvertPtrToArrayRefExpression(node: ConvertPtrToArrayRefExpression)
    {
        node.lValue.visit(this);
    }

    visitVariableRef(node: VariableRef)
    {
    }

    visitIfStatement(node: IfStatement)
    {
        node.conditional.visit(this);
        node.body.visit(this);
        Node1.visit(node.elseBody, this);
    }

    visitWhileLoop(node: WhileLoop)
    {
        node.conditional.visit(this);
        node.body.visit(this);
    }

    visitDoWhileLoop(node: DoWhileLoop)
    {
        node.body.visit(this);
        node.conditional.visit(this);
    }

    visitForLoop(node: ForLoop)
    {
        Node1.visit(node.initialization, this);
        Node1.visit(node.condition, this);
        Node1.visit(node.increment, this);
        node.body.visit(this);
    }

    visitSwitchStatement(node: SwitchStatement)
    {
        node.value.visit(this);
        for (let switchCase of node.switchCases)
            switchCase.visit(this);
    }

    visitSwitchCase(node: SwitchCase)
    {
        Node1.visit(node.value, this);
        node.body.visit(this);
    }

    visitReturn(node: Return)
    {
        Node1.visit(node.value, this);
    }

    visitContinue(node: Continue)
    {
    }

    visitBreak(node: Break)
    {
    }

    visitTrapStatement(node: TrapStatement)
    {
    }

    visitGenericLiteral(node: GenericLiteral)
    {
        node.type.visit(this);
    }

    visitGenericLiteralType(node: GenericLiteralType)
    {
        Node1.visit(node.type, this);
        node.preferredType.visit(this);
    }

    visitNullLiteral(node: NullLiteral)
    {
        node.type.visit(this);
    }

    visitBoolLiteral(node: BoolLiteral)
    {
    }

    visitNullType(node: NullType)
    {
        Node1.visit(node.type, this);
    }

    visitCallExpression(node: CallExpression)
    {
        for (let typeArgument of node.typeArguments)
            typeArgument.visit(this);
        for (let argument of node.argumentList)
            Node1.visit(argument, this);
        let handleTypeArguments = (actualTypeArguments: Type[]) => {
            if (actualTypeArguments) {
                for (let argument of actualTypeArguments)
                    (<Node1>argument).visit(this);
            }
        };
        handleTypeArguments(node.actualTypeArguments);
        handleTypeArguments(node.instantiatedActualTypeArguments);
        Node1.visit(node.nativeFuncInstance, this);
        Node1.visit(node.returnType, this);
        Node1.visit(node.resultType, this);
    }

    visitLogicalNot(node: LogicalNot)
    {
        node.operand.visit(this);
    }

    visitLogicalExpression(node: LogicalExpression)
    {
        node.left.visit(this);
        node.right.visit(this);
    }

    visitFunctionLikeBlock(node: FunctionLikeBlock)
    {
        Node1.visit(node.returnType, this);
        for (let argument of node.argumentList)
            argument.visit(this);
        for (let parameter of node.parameters)
            parameter.visit(this);
        node.body.visit(this);
        Node1.visit(node.resultType, this);
    }

    visitAnonymousVariable(node: AnonymousVariable)
    {
        Node1.visit(node.type, this);
    }

    visitIdentityExpression(node: Node1)
    {
        node.target.visit(this);
    }
}

class GenericLiteral extends Expression {
    _value: number;
    literalClassName: string;
    preferredTypeName: string;
    constructor(origin: Origin, value: number)
    {
        super(origin);
        this._value = value;
        this._isLiteral = true;
    }

    static withType(origin: Origin, value: number, type: Type)
    {
        throw new Error("no withType");
    }

    get value() { return this._value; }

    // This is necessary because once we support int64, we'll need that to be represented as an object
    // rather than as a primitive. Then we'll need to convert.
    get valueForSelectedType(): number
    {
        let type = this.type.type.unifyNode;
        if (!type)
            throw new Error("Cannot get type for " + this);
        let func = (<{[index:string] : (value: number)=>number}><unknown>type)["formatValueFrom" + this.literalClassName];
        if (!func)
            throw new Error("Cannot get function to format type for " + this.literalClassName + " from " + type);
        return <number>func.call(type, this.value);
    }

    get isConstexpr() {
       if (this.hasBecome) return this.target.isConstexpr;
       return true;
    }

    negConstexpr(origin?: Origin): GenericLiteral
    {
       return null;
    }

    unifyImpl(unificationContext: UnificationContext, other: Node1): boolean
    {
        if (!(other instanceof GenericLiteral))
            return false;
        return this.value == other.value;
    }

    toString(): string
    {
        return this.preferredTypeName + "Literal<" + this.value + ">";
    }
}

class DoubleLiteral extends GenericLiteral {
   constructor(origin: Origin, value: number) {
       super(origin, value);
       this.type = new DoubleLiteralType(origin, value);
       this.literalClassName = "DoubleLiteral";
       this.preferredTypeName = "double";
   }

   static withType(origin: Origin, value: number, type: Type)
   {
       let result = new DoubleLiteral(origin, value);
       result.type = TypeRef.wrap(type);
       return result;
   }

   negConstexpr(origin: Origin)
   {
       return new IntLiteral(origin, -this.value);
   }
}

class FloatLiteral extends GenericLiteral {
    constructor(origin: Origin, value: number) {
        super(origin, value);
        this.type = new FloatLiteralType(origin, value);
        this.literalClassName = "FloatLiteral";
        this.preferredTypeName = "FloatLiteral";
    }

    static withType(origin: Origin, value: number, type: Type)
    {
        let result = new FloatLiteral(origin, value);
        result.type = TypeRef.wrap(type);
        return result;
    }

    negConstexpr(origin: Origin)
    {
        return new IntLiteral(origin, -this.value);
    }
}

class IntLiteral extends GenericLiteral {
    constructor(origin: Origin, value: number) {
        super(origin, value);
        this.type = new IntLiteralType(origin, value);
        this.literalClassName = "IntLiteral";
        this.preferredTypeName = "int";
    }

    static withType(origin: Origin, value: number, type: Type)
    {
        let result = new IntLiteral(origin, value);
        result.type = TypeRef.wrap(type);
        return result;
    }

    negConstexpr(origin: Origin)
    {
        return new IntLiteral(origin, (-this.value) | 0);
    }
}

class UintLiteral extends GenericLiteral {
    constructor(origin: Origin, value: number) {
        super(origin, value);
        this.type = new UintLiteralType(origin, value);
        this.literalClassName = "UintLiteral";
        this.preferredTypeName = "uint";
    }

    static withType(origin: Origin, value: number, type: Type)
    {
        let result = new UintLiteral(origin, value);
        result.type = TypeRef.wrap(type);
        return result;
    }

    negConstexpr(origin: Origin)
    {
        return new UintLiteral(origin, (-this.value) | 0);
    }
}

class GenericLiteralType extends Type {
    _value: number;
    preferredType: TypeRef;
    preferredTypeName: string;
    constructor(origin: Origin, value: number, name: string)
    {
        super();
        this._origin = origin;
        this._value = value;
        this.preferredType = new TypeRef(origin, name, []);
        this._isLiteral = true;
    }

    get value() { return this._value; }

    get isPrimitive() { return true; }
    get isUnifiable() { return true; }

    typeVariableUnify(unificationContext: UnificationContext, other: Type)
    {

        if (!(other instanceof Type))
            return false;

        return this._typeVariableUnifyImpl(unificationContext, other);
    }

    unifyImpl(unificationContext: UnificationContext, other: Type)
    {
        return this.typeVariableUnify(unificationContext, other);
    }

    prepareToVerify(unificationContext: UnificationContext): PrepareToVerityType
    {
        let realThis = unificationContext.find(this);
        if (realThis instanceof TypeVariable || realThis.isLiteral) {
            return () => {
                if (realThis.unify(unificationContext, this.preferredType))
                    return {result: true, reason: undefined};
                return {result: false, reason: "Type mismatch between " + unificationContext.find(realThis) + " and " + this.preferredType};
            };
        }
    }

    verifyAsArgument(unificationContext: UnificationContext): VerityResultType
    {
        throw new Error("no verifyAsArgument");
    }

    verifyAsParameter(unificationContext: UnificationContext): VerityResultType
    {
        throw new Error("GenericLiteralType should never be used as a type parameter");
    }

    conversionCost(unificationContext: UnificationContext)
    {
        let realThis = unificationContext.find(this);
        if (realThis.equals(this.preferredType))
            return 0;
        return 1;
    }

    commitUnification(unificationContext: UnificationContext)
    {
        this.type = <Type>unificationContext.find(this).visit(new AutoWrapper());
    }

    toString(): string
    {
        return this.preferredTypeName + "LiteralType<" + this.value + ">";
    }
}

class DoubleLiteralType extends GenericLiteralType {
    constructor(origin: Origin, value: number) {
        super(origin, value, "double");
        this.preferredTypeName =  "double";
    }

    verifyAsArgument(unificationContext: UnificationContext): VerityResultType {
        let realThis = <Type>unificationContext.find(this);
        if (!realThis.isFloating)
            return {result: false, reason: "Cannot use double literal with non-floating type " + realThis};
        if (!realThis.canRepresent(<number>this.value))
            return {result: false, reason: "Float literal " + this.value + " does not fit in type " + realThis};
        return {result: true, reason: undefined};
    }
}

class FloatLiteralType extends GenericLiteralType {
    constructor(origin: Origin, value: number) {
        super(origin, value, "float");
        this.preferredTypeName =  "float";
    }

    verifyAsArgument(unificationContext: UnificationContext): VerityResultType
    {
        let realThis = <Type>unificationContext.find(this);
        if (!realThis.isFloating)
            return {result: false, reason: "Cannot use float literal with non-floating type " + realThis};
        if (!realThis.canRepresent(<number>this.value))
            return {result: false, reason: "Float literal " + this.value + " does not fit in type " + realThis};
        return {result: true, reason: undefined};
    }
}

class IntLiteralType extends GenericLiteralType {
    constructor(origin: Origin, value: number) {
        super(origin, value, "int");
        this.preferredTypeName =  "int";
    }

    verifyAsArgument(unificationContext: UnificationContext): VerityResultType
    {
        let realThis = <Type>unificationContext.find(this);
        if (!realThis.isNumber)
            return {result: false, reason: "Cannot use int literal with non-number type " + realThis};
        if (!realThis.canRepresent(<number>this.value))
            return {result: false, reason: "Int literal " + this.value + " too large to be represented by type " + realThis};
        return {result: true, reason: undefined};
    }
}

class UintLiteralType extends  GenericLiteralType {
    constructor(origin: Origin, value: number) {
        super(origin, value, "uint");
        this.preferredTypeName =  "uint";
    }

    verifyAsArgument(unificationContext: UnificationContext): VerityResultType
    {
        let realThis = <Type>unificationContext.find(this);
        if (!realThis.isInt)
            return {result: false, reason: "Cannot use uint literal with non-number type " + realThis};
        if (realThis.isSigned)
            return {result: false, reason: "Cannot use uint literal with signed type " + realThis};
        if (!realThis.canRepresent(<number>this.value))
            return {result: false, reason: "Uint literal " + this.value + " too large to be represented by type " + realThis};
        return {result: true, reason: undefined};
    }
}

class PropertyAccessExpression extends Expression {
    base: Expression;
    baseType: Type;
    _isLValue: boolean;
    _notLValueReason: string;
    resultTypeForGet: Type;
    resultTypeForAnd: Type;
    callForAnd: CallExpression;
    callForGet: CallExpression;
    callForSet: CallExpression;
    possibleGetOverloads: Node1[];
    possibleSetOverloads: Node1[];
    possibleAndOverloads: Node1[];
    errorForSet: WTypeError;


    constructor(origin: Origin, base: Expression)
    {
        super(origin);
        this.base = base;
        this._isLValue = null; // We use null to indicate that we don't know yet.
        this.addressSpace = null;
        this._notLValueReason = null;
    }

    get getFuncName() { return ""; }
    get andFuncName() { return ""; }
    get setFuncName() { return ""; }

    get resultType() { return this.resultTypeForGet ? this.resultTypeForGet : this.resultTypeForAnd; }
    get isLValue() {
        if (this.hasBecome) return this.target.isLValue;
        return this._isLValue;
    }
    set isLValue(value) { this._isLValue = value; }
    get notLValueReason() { return this._notLValueReason; }
    set notLValueReason(value) { this._notLValueReason = value; }

    rewriteAfterCloning()
    {
        // At this point, node.base.isLValue will only return true if it's possible to get a pointer to it,
        // since we will no longer see any DotExpressions or IndexExpressions. Also, node is a newly created
        // node and everything beneath it is newly created, so we can edit it in-place.

        if ((this.base.isLValue || this.baseType.isRef) && this.callForAnd)
            return new DereferenceExpression(this.origin, this.callForAnd, this.resultType, this.callForAnd.resultType.addressSpace);

        if (this.callForGet)
            return this.callForGet;

        if (!this.callForAnd)
            throw new Error(this.origin.originString + ": Property access without getter and ander: " + this);

        let anonVar = new AnonymousVariable(this.origin, this.baseType);
        this.callForAnd.argumentList[0] = (<Type>this.baseType.unifyNode).argumentForAndOverload(this.origin, VariableRef.wrap(anonVar));
        return new CommaExpression(this.origin, [
            anonVar,
            new Assignment(this.origin, VariableRef.wrap(anonVar), this.base, this.baseType),
            new DereferenceExpression(this.origin, this.callForAnd, this.resultType, this.callForAnd.resultType.addressSpace)
        ]);
    }

    updateCalls()
    {
        if (this.callForGet)
            this.callForGet.argumentList[0] = this.base;
        if (this.callForAnd)
            this.callForAnd.argumentList[0] = this.baseType.argumentForAndOverload(this.origin, this.base);
        if (this.callForSet)
            this.callForSet.argumentList[0] = this.base;
    }

    emitGet(base: Expression)
    {
        let result = <PropertyAccessExpression>this.visit(new Rewriter());
        result.base = base;
        result.updateCalls();
        return result.rewriteAfterCloning();
    }

    emitSet(base: Expression, value: ValueType)
    {
        let result = <PropertyAccessExpression>this.visit(new Rewriter());
        if (!result.callForSet)
            throw new WTypeError(this.origin.originString, "Cannot emit set because: " + this.errorForSet.typeErrorMessage);
        result.base = base;
        result.updateCalls();
        result.callForSet.argumentList[result.callForSet.argumentList.length - 1] = <Node1>value;
        return result.callForSet;
    }
}

const addressSpaces = ["constant", "device", "threadgroup", "thread"];

function isAddressSpace(addressSpace: string)
{
    switch (addressSpace) {
    case "constant":
    case "device":
    case "threadgroup":
    case "thread":
        return true;
    default:
        return false;
    }
}

function validateAddressSpace(addressSpace: string)
{
    if (!isAddressSpace(addressSpace))
        throw new Error("Bad address space: " + addressSpace);
}
/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";

let anonymousVariableCount = 0;

class AnonymousVariable extends Value {
    index: int;
    // You have to initialize the variable's value before use, but that could be quite late.
    constructor(origin: Origin, type: Type = null)
    {
        super();
        this._origin = origin;
        this.index = anonymousVariableCount++;
        this.type = type;
        this.name =  "anonVar<" + this.index + ">";
    }

    toString()
    {
        return this.name;
    }
}

/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";

// Note that we say that T[] is "the same type" as T[c] for any T, c. This greatly simplifies the
// language.
class ArrayRefType extends ReferenceType {
    constructor(origin: Origin, addressSpace: string, elementType: Type) {
        super(origin, addressSpace, elementType);
        this._isArrayRef = true;
    }
    unifyImpl(unificationContext: UnificationContext, other: Type)
    {
        if (!other.isArrayRef)
            return false;

        if (this.addressSpace != other.addressSpace)
            return false;

        return this.elementType.unify(unificationContext, other.elementType);
    }

    argumentForAndOverload(origin: Origin, value: ValueType): Node1
    {
        return <Node1>value;
    }
    argumentTypeForAndOverload(origin: Origin)
    {
        return this;
    }

    toString()
    {
        return this.elementType + "[] " + this.addressSpace;
    }
}

/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";
function ArrayTypePopulate(buffer: EBuffer, offset: int) {
    for (let i = 0; i < this.numElementsValue; ++i)
        this.elementType.populateDefaultValue(buffer, offset + i * this.elementType.size);
}
class ArrayType extends Type {
    _elementType: Type;
    _numElements: Expression;
    constructor(origin: Origin, elementType: Type, numElements: Expression)
    {
        if (!numElements)
            throw new Error("null numElements");
        super();
        this._origin = origin;
        this._elementType = elementType;
        this._numElements = numElements;
        this.populateDefaultValue = ArrayTypePopulate;
    }

    get elementType() { return this._elementType; }
    get numElements() { return this._numElements; }
    get isPrimitive() { return this.elementType.isPrimitive; }
    get isArray() { return true; }

    get numElementsValue(): number
    {
        if (!(this.numElements.isLiteral))
            throw new Error("numElements is not a literal: " + this.numElements);
        return <number>(<GenericLiteral>this.numElements).value;
    }

    toString()
    {
        return this.elementType + "[" + this.numElements + "]";
    }

    get size()
    {
        return this.elementType.size * this.numElementsValue;
    }
    unifyImpl(unificationContext: UnificationContext, other: ArrayType)
    {
        if (!(other instanceof ArrayType))
            return false;

        if (!this.numElements.unify(unificationContext, other.numElements))
            return false;

        return this.elementType.unify(unificationContext, other.elementType);
    }

    argumentForAndOverload(origin: Origin, value: ValueType)
    {
        let result = new MakeArrayRefExpression(origin, <Value>value);
        result.numElements = this.numElements;
        return result;
    }
    argumentTypeForAndOverload(origin: Origin): ReferenceType
    {
        return new ArrayRefType(origin, "thread", this.elementType);
    }
}

class Assignment extends Expression {
    _lhs: Value;
    _rhs: Node1;
    constructor(origin: Origin, lhs: Value, rhs: Node1, type: Type = null)
    {
        super(origin);
        this._lhs = lhs;
        this._rhs = rhs;
        this.type = type;
    }

    get lhs() { return this._lhs; }
    get rhs() { return this._rhs; }

    toString()
    {
        return this.lhs + " = " + this.rhs;
    }
};

/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";

class AutoWrapper extends Rewriter {
    visitVariableRef(node: VariableRef)
    {
        return node;
    }

    visitTypeRef(node: TypeRef)
    {
        return node;
    }

    visitConstexprTypeParameter(node: ConstexprTypeParameter)
    {
        return VariableRef.wrap(node);
    }

    visitFuncParameter(node: FuncParameter)
    {
        return VariableRef.wrap(node);
    }

    visitVariableDecl(node: VariableDecl)
    {
        return VariableRef.wrap(node);
    }

    visitStructType(node: StructType)
    {
        return TypeRef.wrap(node);
    }

    visitNativeType(node: NativeType)
    {
        return TypeRef.wrap(node);
    }

    visitTypeVariable(node: TypeVariable)
    {
        return TypeRef.wrap(<Type><unknown>node);
    }

    visitGenericLiteralType(node: GenericLiteralType)
    {
        return node;
    }

    visitNullType(node: NullType)
    {
        return node;
    }
}

class Block extends Node1 {
    _statements: Node1[];
    __origin: string;
    constructor(origin: string)
    {
        super();
        this.__origin = origin;
        this._statements = [];
    }

    add(statement: Node1)
    {
        this._statements.push(statement);
    }

    get statements() { return this._statements; }

    toString()
    {
        if (!this.statements.length)
            return "{ }";
        return "{ " + this.statements.join("; ") + "; }";
    }
};

/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";

class BoolLiteral extends Expression {
    _value: boolean;
    constructor(origin: Origin, value: boolean)
    {
        super(origin);
        this._value = value;
    }

    get value() { return this._value; }
    get isConstexpr() {
        if (this.hasBecome) return this.target.isConstexpr;
        return true;
    }

    toString()
    {
        return "" + this._value;
    }
}

/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";

class Break extends Node1 {
    constructor(origin: Origin)
    {
        super();
        this._origin = origin;
    }

    toString()
    {
        return "break";
    }
};

/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";

class CallExpression extends Expression {
    _returnType: Type;
    _name: string;
    _typeArguments: Node1[];
    _argumentList : Node1[];
    func: Func;
    _isCast: boolean;
    argumentTypes: Type[];
    actualTypeArguments: Type[];
    instantiatedActualTypeArguments: Type[]
    possibleOverloads: Func[];
    resultType: Type;
    nativeFuncInstance: NativeFuncInstance;
    resultEPtr: EPtr;

    constructor(origin: Origin, name: string, typeArguments: Node1[], argumentList: Node1[])
    {
        super(origin);
        this._name = name;
        this._typeArguments = typeArguments;
        this._argumentList = argumentList;
        this.func = null;
        this._isCast = false;
        this._returnType = null;
    }

    get typeArguments() { return this._typeArguments; }
    get argumentList() { return this._argumentList; }
    get isCast() { return this._isCast; }
    get returnType(): Type { return this._returnType; }

    static resolve(origin: Origin, possibleOverloads: Func[], typeParametersInScope: Node1[], name: string, typeArguments: Type[], argumentList: Node1[], argumentTypes: Type[], returnType: TypeRef): CallResolve
    {
        let call = new CallExpression(origin, name, typeArguments, argumentList);
        call.argumentTypes = <Type[]>argumentTypes.map(argument => argument.visit(new AutoWrapper()));
        call.possibleOverloads = possibleOverloads;
        if (returnType)
            call.setCastData(returnType);
        return {call, resultType: call.resolve(possibleOverloads, typeParametersInScope, typeArguments)};
    }

    resolve(possibleOverloads: Func[], typeParametersInScope: Node1[], typeArguments: Type[])
    {
        if (!possibleOverloads)
            throw new WTypeError(this.origin.originString, "Did not find any functions named " + this.name);

        let overload: OverLoad= null;
        let failures = [];
        for (let typeParameter of typeParametersInScope) {
            if (!(typeParameter instanceof TypeVariable))
                continue;
            if (!typeParameter.protocol)
                continue;
            let signatures: Func[] =
                <Func[]>typeParameter.protocol.protocolDecl.signaturesByNameWithTypeVariable(this.name, typeParameter);
            if (!signatures)
                continue;
            overload = <OverLoad>resolveOverloadImpl(signatures, this.typeArguments, this.argumentTypes, this.returnType);
            if (overload.func)
                break;
            failures.push(...(<OverLoadFailure>overload).failures);
            overload = null;
        }
        if (!overload) {
            overload = <OverLoad>resolveOverloadImpl(
                possibleOverloads, this.typeArguments, this.argumentTypes, this.returnType);
            if (!overload.func) {
                failures.push(...(<OverLoadFailure>overload).failures);
                let message = "Did not find function named " + this.name + " for call with ";
                if (this.typeArguments.length)
                    message += "type arguments <" + this.typeArguments + "> and ";
                message += "argument types (" + this.argumentTypes + ")";
                if (this.returnType)
                    message +=" and return type " + this.returnType;
                if (failures.length)
                    message += ", but considered:\n" + failures.join("\n")
                throw new WTypeError(this.origin.originString, message);
            }
        }
        overload = <OverLoadSuccess>overload;
        for (let i = 0; i < typeArguments.length; ++i) {
            let typeArgumentType = typeArguments[i];
            let typeParameter = overload.func.typeParameters[i];
            if (!(typeParameter instanceof ConstexprTypeParameter))
                continue;
            if (!typeParameter.type.equalsWithCommit(typeArgumentType))
                throw new Error("At " + this.origin.originString + " constexpr type argument and parameter types not equal: argument = " + typeArgumentType + ", parameter = " + typeParameter.type);
        }
        for (let i = 0; i < this.argumentTypes.length; ++i) {
            let argumentType = this.argumentTypes[i];
            let parameterType = overload.func.parameters[i].type.substituteToUnification(
                overload.func.typeParameters, overload.unificationContext);
            let result = argumentType.equalsWithCommit(parameterType);
            if (!result)
                throw new Error("At " + this.origin.originString + " argument and parameter types not equal after type argument substitution: argument = " + argumentType + ", parameter = " + parameterType);
        }
        return this.resolveToOverload(overload);
    }

    resolveToOverload(overload: OverLoadSuccess)
    {
        this.func = overload.func;
        this.actualTypeArguments = <Type[]>overload.typeArguments.map(typeArgument => typeArgument instanceof Type ? typeArgument.visit(new AutoWrapper()) : typeArgument);
        this.instantiatedActualTypeArguments = this.actualTypeArguments;
        let result: Type = <Type>overload.func.returnType.substituteToUnification(
            overload.func.typeParameters, overload.unificationContext);
        if (!result)
            throw new Error("Null return type");
        result = <Type>result.visit(new AutoWrapper());
        this.resultType = <TypeRef>result; // XTS
        return result;
    }

    becomeCast(returnType: Type)
    {
        this._returnType = new TypeRef(this.origin, this.name, this._typeArguments);
        this._returnType.type = returnType;
        this._name = "operator cast";
        this._isCast = true;
        this._typeArguments = [];
    }

    setCastData(returnType: Type)
    {
        this._returnType = returnType;
        this._isCast = true;
    }

    toString()
    {
        return (this.isCast ? "operator " + this.returnType : this.name) +
            "<" + this.typeArguments + ">" +
            (this.actualTypeArguments ? "<<" + this.actualTypeArguments + ">>" : "") +
            "(" + this.argumentList + ")";
    }
}

// This allows you to pass structs and arrays in-place, but it's a more annoying API.
function callFunction(program: Program, name: string, typeArguments: Type[], argumentList: TypedValue[])
{
    let argumentTypes = argumentList.map((argument: TypedValue) => argument.type);
    let funcOrFailures = <OverloadResolutionFailure[] | Func>resolveInlinedFunction(program, name, typeArguments, argumentTypes, true);
    if (!(funcOrFailures instanceof Func)) {
        let failures = <OverloadResolutionFailure[]>funcOrFailures;
        throw new WTypeError("<callFunction>", "Cannot resolve function call " + name + "<" + typeArguments + ">(" + argumentList + ")" + (failures.length ? "; tried:\n" + failures.join("\n") : ""));
    }
    let func: Func = funcOrFailures;
    for (let i = 0; i < func.parameters.length; ++i) {
        let type = argumentTypes[i].instantiatedType;
        type.visit(new StructLayoutBuilder());
        func.parameters[i].ePtr.copyFrom(argumentList[i].ePtr, type.size);
    }
    let result = new Evaluator(program).runFunc(<FuncDef>func);
    return new TypedValue(func.uninstantiatedReturnType, result);
}

function check(program: Program)
{
    program.visit(new Checker(program));
}

function checkLiteralTypes(program: Program)
{
    program.visit(new LiteralTypeChecker());
}
/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";

function checkLoops(program: Program)
{
    program.visit(new LoopChecker());
}

/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";

function checkRecursiveTypes(program: Program)
{
    program.visit(new RecursiveTypeChecker());
}
/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";

function checkRecursion(program: Program)
{
    program.visit(new RecursionChecker(program));
}

/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";

function checkReturns(program: Program)
{
    program.visit(new ReturnChecker(program));
}

/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";

function checkUnreachableCode(program: Program)
{
    program.visit(new UnreachableCodeChecker(program));
}

/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";

function checkExpressionWrapped(node: Node1)
{
    node.visit(new WrapChecker(node));
}

function checkProgramWrapped(node: Node1)
{
    node.visit(new ExpressionFinder(checkExpressionWrapped));
}

/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";

class Checker extends Visitor {
    _program: Program;
    _currentStatement: Node1;
    _vertexEntryPoints: Set<string>;
    _fragmentEntryPoints: Set<string>

    constructor(program: Program)
    {
        super();
        this._program = program;
        this._currentStatement = null;
        this._vertexEntryPoints = new Set();
        this._fragmentEntryPoints = new Set();
    }

    doStatement(statement: Node1) {
        this._currentStatement = statement;
        statement.visit(this);
    }

    visitProgram(node: Program)
    {
        for (let type of node.types.values())
            this.doStatement(type);
        for (let protocol of node.protocols.values())
            this.doStatement(protocol);
        for (let funcs of node.functions.values()) {
            for (let func of funcs) {
                this.visitFunc(func);
            }
        }
        for (let funcs of node.functions.values()) {
            for (let func of funcs)
                this.doStatement(func);
        }
    }

    _checkShaderType(node: Func)
    {
        // FIXME: Relax these checks once we have implemented support for textures and samplers.
        if (node.typeParameters.length != 0)
            throw new WTypeError(node.origin.originString, "Entry point " + node.name + " must not have type arguments.");
        let shaderFunc = node;
        switch (node.shaderType) {
        case "vertex":
            if (this._vertexEntryPoints.has(node.name))
                throw new WTypeError(node.origin.originString, "Duplicate vertex entry point name " + node.name);
            this._vertexEntryPoints.add(node.name);
            break;
        case "fragment":
            if (this._fragmentEntryPoints.has(node.name))
                throw new WTypeError(node.origin.originString, "Duplicate fragment entry point name " + node.name);
            this._fragmentEntryPoints.add(node.name);
            break;
        }
    }

    _checkOperatorOverload(func: Func, resolveFuncs: (name: string)=>Func[])
    {
        if (Lexer.textIsIdentifier(func.name))
            return; // Not operator!

        if (!func.name.startsWith("operator"))
            throw new Error("Bad operator overload name: " + func.name);

        let typeVariableTracker = new TypeVariableTracker();
        for (let parameterType of func.parameterTypes)
            parameterType.visit(typeVariableTracker);
        Node1.visit(func.returnTypeForOverloadResolution, typeVariableTracker);
        for (let typeParameter of func.typeParameters) {
            if (!typeVariableTracker.set.has(typeParameter))
                throw new WTypeError(typeParameter.origin.originString, "Type parameter " + typeParameter + " to operator " + func.toDeclString() + " is not inferrable from value parameters");
        }

        let checkGetter = (kind: string) => {
            let numExpectedParameters = kind == "index" ? 2 : 1;
            if (func.parameters.length != numExpectedParameters)
                throw new WTypeError(func.origin.originString, "Incorrect number of parameters for " + func.name + " (expected " + numExpectedParameters + ", got " + func.parameters.length + ")");
            if (func.parameterTypes[0].unifyNode.isPtr)
                throw new WTypeError(func.origin.originString, "Cannot have getter for pointer type: " + func.parameterTypes[0]);
        };

        let checkSetter = (kind: string) => {
            let numExpectedParameters = kind == "index" ? 3 : 2;
            if (func.parameters.length != numExpectedParameters)
                throw new WTypeError(func.origin.originString, "Incorrect number of parameters for " + func.name + " (expected " + numExpectedParameters + ", got " + func.parameters.length + ")");
            if (func.parameterTypes[0].unifyNode.isPtr)
                throw new WTypeError(func.origin.originString, "Cannot have setter for pointer type: " + func.parameterTypes[0]);
            if (!func.returnType.equals(func.parameterTypes[0]))
                throw new WTypeError(func.origin.originString, "First parameter type and return type of setter must match (parameter was " + func.parameterTypes[0] + " but return was " + func.returnType + ")");
            let valueType = func.parameterTypes[numExpectedParameters - 1];
            let getterName: string = func.name.substr(0, func.name.length - 1);
            let getterFuncs = resolveFuncs(getterName);
            if (!getterFuncs)
                throw new WTypeError(func.origin.originString, "Every setter must have a matching getter, but did not find any function named " + getterName + " to match " + func.name);
            let argumentTypes = func.parameterTypes.slice(0, numExpectedParameters - 1);
            let overload: OverLoad = <OverLoad>resolveOverloadImpl(getterFuncs, [], argumentTypes, null);
            if (!overload.func) {
                throw new WTypeError(func.origin.originString, "Did not find function named " + func.name + " with arguments " + argumentTypes + ((<OverLoadFailure>overload).failures.length ? "; tried:\n" + (<OverLoadFailure>overload).failures.join("\n") : ""));
            }
            overload = <OverLoadSuccess>(overload);
            let resultType = overload.func.returnType.substituteToUnification(
                overload.func.typeParameters, overload.unificationContext);
            if (!resultType.equals(valueType))
                throw new WTypeError(func.origin.originString, "Setter and getter must agree on value type (getter at " + overload.func.origin.originString + " says " + resultType + " while this setter says " + valueType + ")");
        };

        let checkAnder = (kind: string) => {
            let numExpectedParameters = kind == "index" ? 2 : 1;
            if (func.parameters.length != numExpectedParameters)
                throw new WTypeError(func.origin.originString, "Incorrect number of parameters for " + func.name + " (expected " + numExpectedParameters + ", got " + func.parameters.length + ")");
            if (!func.returnType.unifyNode.isPtr)
                throw new WTypeError(func.origin.originString, "Return type of ander is not a pointer: " + func.returnType);
            if (!(<Type>func.parameterTypes[0].unifyNode).isRef)
                throw new WTypeError(func.origin.originString, "Parameter to ander is not a reference: " + func.parameterTypes[0]);
        };

        switch (func.name) {
        case "operator cast":
            break;
        case "operator++":
        case "operator--":
            if (func.parameters.length != 1)
                throw new WTypeError(func.origin.originString, "Incorrect number of parameters for " + func.name + " (expected 1, got " + func.parameters.length + ")");
            if (!func.parameterTypes[0].equals(func.returnType))
                throw new WTypeError(func.origin.originString, "Parameter type and return type must match for " + func.name + " (parameter is " + func.parameterTypes[0] + " while return is " + func.returnType + ")");
            break;
        case "operator+":
        case "operator-":
            if (func.parameters.length != 1 && func.parameters.length != 2)
                throw new WTypeError(func.origin.originString, "Incorrect number of parameters for " + func.name + " (expected 1 or 2, got " + func.parameters.length + ")");
            break;
        case "operator*":
        case "operator/":
        case "operator%":
        case "operator&":
        case "operator|":
        case "operator^":
        case "operator<<":
        case "operator>>":
            if (func.parameters.length != 2)
                throw new WTypeError(func.origin.originString, "Incorrect number of parameters for " + func.name + " (expected 2, got " + func.parameters.length + ")");
            break;
        case "operator~":
            if (func.parameters.length != 1)
                throw new WTypeError(func.origin.originString, "Incorrect number of parameters for " + func.name + " (expected 1, got " + func.parameters.length + ")");
            break;
        case "operator==":
        case "operator<":
        case "operator<=":
        case "operator>":
        case "operator>=":
            if (func.parameters.length != 2)
                throw new WTypeError(func.origin.originString, "Incorrect number of parameters for " + func.name + " (expected 2, got " + func.parameters.length + ")");
            if (!func.returnType.equals(this._program.intrinsics.bool))
                throw new WTypeError(func.origin.originString, "Return type of " + func.name + " must be bool but was " + func.returnType);
            break;
        case "operator[]":
            checkGetter("index");
            break;
        case "operator[]=":
            checkSetter("index");
            break;
        case "operator&[]":
            checkAnder("index");
            break;
        default:
            if (func.name.startsWith("operator.")) {
                if (func.name.endsWith("="))
                    checkSetter("dot");
                else
                    checkGetter("dot");
                break;
            }
            if (func.name.startsWith("operator&.")) {
                checkAnder("dot");
                break;
            }
            throw new Error("Parser accepted unrecognized operator: " + func.name);
        }
    }

    visitFuncDef(node: FuncDef)
    {
        if (node.shaderType)
            this._checkShaderType(node);
        this._checkOperatorOverload(node, name => this._program.functions.get(name));
        node.body.visit(this);
    }

    visitNativeFunc(node: NativeFunc)
    {
    }

    visitProtocolDecl(node: ProtocolDecl)
    {
        for (let signature of node.signatures) {
            let typeVariableTracker = new TypeVariableTracker();
            for (let parameterType of signature.parameterTypes)
                parameterType.visit(typeVariableTracker);
            Node1.visit(signature.returnTypeForOverloadResolution, typeVariableTracker);
            for (let typeParameter of signature.typeParameters) {
                if (!typeVariableTracker.set.has(typeParameter))
                    throw new WTypeError(typeParameter.origin.originString, "Type parameter to protocol signature not inferrable from value parameters");
            }
            if (!typeVariableTracker.set.has(node.typeVariable))
                throw new WTypeError(signature.origin.originString, "Protocol's type variable (" + node.name + ") not mentioned in signature: " + signature);
            this._checkOperatorOverload(signature, name => node.signaturesByName(name));
        }
    }

    visitEnumType(node: EnumType)
    {
        node.baseType.visit(this);

        let baseType = <Type>node.baseType.unifyNode;

        if (!baseType.isInt)
            throw new WTypeError(node.origin.originString, "Base type of enum is not an integer: " + node.baseType);

        for (let member of node.members) {
            if (!member.value)
                continue;

            let memberType = (<Node1>member.value).visit(this);
            if (!baseType.equalsWithCommit(<Node1>memberType))
                throw new WTypeError(member.origin.originString, "Type of enum member " + member.value.name + " does not patch enum base type (member type is " + memberType + ", enum base type is " + node.baseType + ")");
        }

        let nextValue = baseType.defaultValue;
        for (let member of node.members) {
            if (member.value) {
                nextValue = baseType.successorValue((<GenericLiteral>member.value.unifyNode).valueForSelectedType);
                continue;
            }

            member.value = baseType.createLiteral(member.origin, nextValue);
            nextValue = baseType.successorValue(nextValue);
        }

        let memberArray = Array.from(node.members);
        for (let i = 0; i < memberArray.length; ++i) {
            let member = memberArray[i];
            for (let j = i + 1; j < memberArray.length; ++j) {
                let otherMember = memberArray[j];
                if (baseType.valuesEqual((<GenericLiteral>member.value.unifyNode).valueForSelectedType, (<GenericLiteral>otherMember.value.unifyNode).valueForSelectedType))
                    throw new WTypeError(otherMember.origin.originString, "Duplicate enum member value (" + member.name + " has " + member.value + " while " + otherMember.name + " has " + otherMember.value + ")");
            }
        }

        let foundZero = false;
        for (let member of node.members) {
            if (baseType.valuesEqual((<GenericLiteral>member.value.unifyNode).valueForSelectedType, baseType.defaultValue)) {
                foundZero = true;
                break;
            }
        }
        if (!foundZero)
            throw new WTypeError(node.origin.originString, "Enum does not have a member with the value zero");
    }

    _checkTypeArguments(origin: Origin, typeParameters: Node1[], typeArguments: Node1[])
    {
        for (let i = 0; i < typeParameters.length; ++i) {
            let argumentIsType = typeArguments[i] instanceof Type;
            let result = typeArguments[i].visit(this);
            if (argumentIsType) {
                let result = <{result: true, reason: string}>(<TypeVariable>typeArguments[i]).inherits((<TypeVariable>typeParameters[i]).protocol);
                if (!result.result)
                    throw new WTypeError(origin.originString, "Type argument does not inherit protocol: " + result.reason);
            } else {
                if (!(<Node1>result).equalsWithCommit((<ConstexprTypeParameter>typeParameters[i]).type))
                    throw new WTypeError(origin.originString, "Wrong type for constexpr");
            }
        }
    }

    visitTypeRef(node: TypeRef)
    {
        if (!node.type)
            throw new Error("Type reference without a type in checker: " + node + " at " + node.origin);
        if (!(node.type instanceof StructType))
            node.type.visit(this);
        this._checkTypeArguments(node.origin, node.type.typeParameters, node.typeArguments);
    }

    visitArrayType(node: ArrayType)
    {
        node.elementType.visit(this);

        if (!node.numElements.isConstexpr)
            throw new WTypeError(node.origin.originString, "Array length must be constexpr");

        let type: Node1 = <Node1>node.numElements.visit(this);

        if (!type.equalsWithCommit(this._program.intrinsics.uint32))
            throw new WTypeError(node.origin.originString, "Array length must be a uint32");
    }

    visitVariableDecl(node: VariableDecl)
    {
        node.type.visit(this);
        if (node.initializer) {
            let lhsType = node.type;
            let rhsType = <Node1>node.initializer.visit(this);
            if (!lhsType.equalsWithCommit(rhsType))
                throw new WTypeError(node.origin.originString, "Type mismatch in variable initialization: " + lhsType + " versus " + rhsType);
        }
    }

    visitAssignment(node: Assignment)
    {
        let lhsType = <Type>node.lhs.visit(this);
        if (!node.lhs.isLValue)
            throw new WTypeError(node.origin.originString, "LHS of assignment is not an LValue: " + node.lhs + node.lhs.notLValueReasonString);
        let rhsType = <Type>node.rhs.visit(this);
        if (!lhsType.equalsWithCommit(rhsType))
            throw new WTypeError(node.origin.originString, "Type mismatch in assignment: " + lhsType + " versus " + rhsType);
        node.type = lhsType;
        return lhsType;
    }

    visitIdentityExpression(node: Node1)
    {
        return node.target.visit(this);
    }

    visitReadModifyWriteExpression(node: ReadModifyWriteExpression)
    {
        let lhsType = <Type>node.lValue.visit(this);
        if (!node.lValue.isLValue)
            throw new WTypeError(node.origin.originString, "LHS of read-modify-write is not an LValue: " + node.lValue + node.lValue.notLValueReasonString);
        node.oldValueVar.type = lhsType;
        node.newValueVar.type = lhsType;
        node.oldValueVar.visit(this);
        node.newValueVar.visit(this);
        let newValueType = <Type>node.newValueExp.visit(this);
        if (!lhsType.equalsWithCommit(newValueType))
            return new WTypeError(node.origin.originString, "Type mismatch in read-modify-write: " + lhsType + " versus " + newValueType);
        return node.resultExp.visit(this);
    }

    visitAnonymousVariable(node: AnonymousVariable)
    {
        if (!node.type)
            throw new Error("Anonymous variable must know type before first appearance");
    }

    visitDereferenceExpression(node: DereferenceExpression)
    {
        let type = <Type>(<Node1>node.ptr.visit(this)).unifyNode;
        if (!type.isPtr)
            throw new WTypeError(node.origin.originString, "Type passed to dereference is not a pointer: " + type);
        node.type = type.elementType;
        node.addressSpace = type.addressSpace;
        if (!node.addressSpace)
            throw new Error("Null address space in type: " + type);
        return node.type;
    }

    visitMakePtrExpression(node: MakePtrExpression)
    {
        let elementType = <Type>(<Node1>node.lValue.visit(this)).unifyNode;
        if (!(<Value>node.lValue).isLValue)
            throw new WTypeError(node.origin.originString, "Operand to & is not an LValue: " + node.lValue + (<Value>node.lValue).notLValueReasonString);

        return new PtrType(node.origin, node.lValue.addressSpace, elementType);
    }

    visitMakeArrayRefExpression(node: MakeArrayRefExpression)
    {
        let elementType = <Type>(<Type>node.lValue.visit(this)).unifyNode;
        if (elementType.isPtr) {
            node.become(new ConvertPtrToArrayRefExpression(node.origin, node.lValue));
            let addressSpace_ = elementType.addressSpace;
            let ptrType = <PtrType>(elementType);
            let elementType_ = ptrType.elementType;
            return new ArrayRefType(node.origin, addressSpace_, elementType_);
        }

        if (!(<Value>node.lValue).isLValue)
            throw new WTypeError(node.origin.originString, "Operand to @ is not an LValue: " + node.lValue + node.lValue.notLValueReasonString);

        if (elementType.isArray) {
            node.numElements = (<ArrayType>elementType).numElements;
            elementType = (<ArrayType>elementType).elementType;
        } else
            node.numElements = UintLiteral.withType(node.origin, 1, this._program.intrinsics.uint32);

        return new ArrayRefType(node.origin, node.lValue.addressSpace, elementType);
    }

    visitConvertToArrayRefExpression(node: ConvertPtrToArrayRefExpression)
    {
        throw new Error("Should not exist yet.");
    }

    _finishVisitingPropertyAccess(node: PropertyAccessExpression, baseType: Type, extraArgs: Node1[], extraArgTypes: Type[])
    {
        baseType = <Type>baseType.visit(new AutoWrapper())
        node.baseType = baseType;

        // Such a type must exist. This may throw if it doesn't.
        let typeForAnd = baseType.argumentTypeForAndOverload(node.origin);
        if (!typeForAnd)
            throw new Error("Cannot get typeForAnd");

        let errorForGet: WTypeError;
        let errorForAnd: WTypeError;

        try {
            let result: CallResolve = <CallResolve>CallExpression.resolve(
                node.origin, <Func[]>node.possibleGetOverloads, (<Func>this._currentStatement).typeParameters,
                node.getFuncName, [], [node.base, ...extraArgs], [baseType, ...extraArgTypes], null);
            node.callForGet = result.call;
            node.resultTypeForGet = result.resultType;
        } catch (e) {
            if (!(e instanceof WTypeError))
                throw e;
            errorForGet = e;
        }

        try {
            let baseForAnd = baseType.argumentForAndOverload(node.origin, node.base);

            let result: CallResolve = <CallResolve>CallExpression.resolve(
                node.origin, <Func[]>node.possibleAndOverloads, (<Func>this._currentStatement).typeParameters,
                node.andFuncName, [], [baseForAnd, ...extraArgs], [typeForAnd, ...extraArgTypes],
                null);
            node.callForAnd = result.call;
            node.resultTypeForAnd = <TypeRef>(<Type>result.resultType.unifyNode).returnTypeFromAndOverload(node.origin);
        } catch (e) {
            if (!(e instanceof WTypeError))
                throw e;
            errorForAnd = e;
        }

        if (!node.resultTypeForGet && !node.resultTypeForAnd) {
            throw new WTypeError(
                node.origin.originString,
                "Cannot resolve access; tried by-value:\n" +
                errorForGet.typeErrorMessage + "\n" +
                "and tried by-pointer:\n" +
                errorForAnd.typeErrorMessage);
        }

        if (node.resultTypeForGet && node.resultTypeForAnd
            && !node.resultTypeForGet.equals(node.resultTypeForAnd))
            throw new WTypeError(node.origin.originString, "Result type resolved by-value (" + node.resultTypeForGet + ") does not match result type resolved by-pointer (" + node.resultTypeForAnd + ")");

        try {
            let result: CallResolve = <CallResolve>CallExpression.resolve(
                node.origin, <Func[]>node.possibleSetOverloads, (<Func>this._currentStatement).typeParameters,
                node.setFuncName, [], [node.base, ...extraArgs, null], [baseType, ...extraArgTypes, node.resultType], null);
            node.callForSet = result.call;
            if (!result.resultType.equals(baseType))
                throw new WTypeError(node.origin.originString, "Result type of setter " + result.call.func + " is not the base type " + baseType);
        } catch (e) {
            if (!(e instanceof WTypeError))
                throw e;
            node.errorForSet = e;
        }

        // OK, now we need to determine if we are an lvalue. We are an lvalue if we can be assigned to. We can
        // be assigned to if we have an ander or setter. But it's weirder than that. We also need the base to be
        // an lvalue, except unless the base is an array reference.
        if (!node.callForAnd && !node.callForSet) {
            node.isLValue = false;
            node.notLValueReason =
                "Have neither ander nor setter. Tried setter:\n" +
                node.errorForSet.typeErrorMessage + "\n" +
                "and tried ander:\n" +
                errorForAnd.typeErrorMessage;
        } else if (!node.base.isLValue && !baseType.isArrayRef) {
            node.isLValue = false;
            node.notLValueReason = "Base of property access is neither a lvalue nor an array reference";
        } else {
            node.isLValue = true;
            node.addressSpace = node.base.isLValue ? node.base.addressSpace : baseType.addressSpace;
        }

        return node.resultType;
    }

    visitDotExpression(node: DotExpression)
    {
        let structType = <StructType>(<StructType>node.struct1.visit(this)).unifyNode;
        return this._finishVisitingPropertyAccess(node, structType, [], []);
    }

    visitIndexExpression(node: IndexExpression)
    {
        let arrayType = <Type>(<Type>node.array.visit(this)).unifyNode;
        let indexType = <Type>node.index.visit(this);

        return this._finishVisitingPropertyAccess(node, arrayType, [node.index], [indexType]);
    }

    visitVariableRef(node: VariableRef)
    {
        if (!node.variable.type)
            throw new Error("Variable has no type: " + node.variable);
        return node.variable.type;
    }

    visitReturn(node: Return)
    {
        if (node.value) {
            let resultType = <Type>node.value.visit(this);
            if (!resultType)
                throw new Error("Null result type from " + node.value);
            if (!node.func.returnType.equalsWithCommit(resultType))
                throw new WTypeError(node.origin.originString, "Trying to return " + resultType + " in a function that returns " + node.func.returnType);
            return;
        }

        if (!node.func.returnType.equalsWithCommit(this._program.intrinsics.void))
            throw new WTypeError(node.origin.originString, "Non-void function must return a value");
    }

    visitGenericLiteral(node: GenericLiteral)
    {
        return node.type;
    }

    visitNullLiteral(node: NullLiteral)
    {
        return node.type;
    }

    visitBoolLiteral(node: BoolLiteral)
    {
        return this._program.intrinsics.bool;
    }

    visitEnumLiteral(node: EnumLiteral)
    {
        return node.member.enumType;
    }

    _requireBool(expression: Expression)
    {
        let type = <Type>expression.visit(this);
        if (!type)
            throw new Error("Expression has no type, but should be bool: " + expression);
        if (!type.equals(this._program.intrinsics.bool))
            throw new WTypeError(expression.origin.originString, "Expression isn't a bool: " + expression);
    }

    visitLogicalNot(node: LogicalNot)
    {
        this._requireBool(node.operand);
        return this._program.intrinsics.bool;
    }

    visitLogicalExpression(node: LogicalExpression)
    {
        this._requireBool(node.left);
        this._requireBool(node.right);
        return this._program.intrinsics.bool;
    }

    visitIfStatement(node: IfStatement)
    {
        this._requireBool(node.conditional);
        node.body.visit(this);
        if (node.elseBody)
            node.elseBody.visit(this);
    }

    visitWhileLoop(node: WhileLoop)
    {
        this._requireBool(node.conditional);
        node.body.visit(this);
    }

    visitDoWhileLoop(node: DoWhileLoop)
    {
        node.body.visit(this);
        this._requireBool(node.conditional);
    }

    visitForLoop(node: ForLoop)
    {
        if (node.initialization)
            node.initialization.visit(this);
        if (node.condition)
            this._requireBool(node.condition);
        if (node.increment)
            node.increment.visit(this);
        node.body.visit(this);
    }

    visitSwitchStatement(node: SwitchStatement)
    {
        let type = <Type>(<Node1>node.value.visit(this)).commit();

        if (!(<Type>type.unifyNode).isInt && !(type.unifyNode instanceof EnumType))
            throw new WTypeError(node.origin.originString, "Cannot switch on non-integer/non-enum type: " + type);

        node.type = type;

        let hasDefault = false;

        for (let switchCase of node.switchCases) {
            switchCase.body.visit(this);

            if (switchCase.isDefault) {
                hasDefault = true;
                continue;
            }

            if (!switchCase.value.isConstexpr)
                throw new WTypeError(switchCase.origin.originString, "Switch case not constexpr: " + switchCase.value);

            let caseType = <Type>switchCase.value.visit(this);
            if (!type.equalsWithCommit(caseType))
                throw new WTypeError(switchCase.origin.originString, "Switch case type does not match switch value type (case type is " + caseType + " but switch value type is " + type + ")");
        }

        for (let i = 0; i < node.switchCases.length; ++i) {
            let firstCase = node.switchCases[i];
            for (let j = i + 1; j < node.switchCases.length; ++j) {
                let secondCase = node.switchCases[j];

                if (firstCase.isDefault != secondCase.isDefault)
                    continue;

                if (firstCase.isDefault)
                    throw new WTypeError(secondCase.origin.originString, "Duplicate default case in switch statement");

                let valuesEqual = (<Type>type.unifyNode).valuesEqual(
                    (<GenericLiteral|EnumLiteral>firstCase.value.unifyNode).valueForSelectedType,
                    (<GenericLiteral|EnumLiteral>secondCase.value.unifyNode).valueForSelectedType);
                if (valuesEqual)
                    throw new WTypeError(secondCase.origin.originString, "Duplicate case in switch statement for value " + (<GenericLiteral>firstCase.value.unifyNode).valueForSelectedType);
            }
        }

        if (!hasDefault) {
            let includedValues = new Set();
            for (let switchCase of node.switchCases)
                includedValues.add((<GenericLiteral|EnumLiteral>switchCase.value.unifyNode).valueForSelectedType);

            for (let {value, name} of (<Type>type.unifyNode).allValues()) {
                if (!includedValues.has(value))
                    throw new WTypeError(node.origin.originString, "Value not handled by switch statement: " + name);
            }
        }
    }

    visitCommaExpression(node: CommaExpression)
    {
        let result: Node1 = null;
        for (let expression of node.list)
            result = <Node1>(<Node1>expression).visit(this);
        return result;
    }

    visitCallExpression(node: CallExpression)
    {
        let typeArguments = <Type[]>node.typeArguments.map(typeArgument => typeArgument.visit(this));
        let argumentTypes: TypeRef[] = <TypeRef[]>node.argumentList.map(argument => {
            let newArgument = <Node1>argument.visit(this);
            if (!newArgument)
                throw new Error("visitor returned null for " + argument);
            return newArgument.visit(new AutoWrapper());
        });

        node.argumentTypes = argumentTypes;
        if (node.returnType)
            node.returnType.visit(this);

        let result = node.resolve(node.possibleOverloads, (<Func>this._currentStatement).typeParameters, typeArguments);
        return result;
    }
}

function cloneProgram(program: Program)
{
    let result = new Program();
    let cloner = new StatementCloner();
    for (let statement of program.topLevelStatements)
        result.add(<Node1>statement.visit(cloner));
    return result;
}

class CommaExpression extends Expression {
    _list: Expression[];
    constructor(origin: Origin, list: Expression[])
    {
        super(origin);
        this._list = list;
        for (let expression of list) {
            if (!expression)
                throw new Error("null expression");
        }
    }

    get list() { return this._list; }

    // NOTE: It's super tempting to say that CommaExpression is an lValue if its last entry is an lValue. But,
    // PropertyResolver relies on this not being the case.

    toString()
    {
        return "(" + this.list.toString() + ")";
    }
}

class ConstexprFolder extends Visitor {
    visitCallExpression(node: CallExpression)
    {
        super.visitCallExpression(node);

        if (node.name == "operator-"
            && !node.typeArguments.length
            && node.argumentList.length == 1
            && (<Value>node.argumentList[0].unifyNode).isConstexpr
            && (<GenericLiteral>node.argumentList[0].unifyNode).negConstexpr) {
            node.become((<GenericLiteral>node.argumentList[0].unifyNode).negConstexpr(node.origin));
            return;
        }
    }

    visitTypeOrVariableRef(node: TypeOrVariableRef)
    {
    }
}

class ConstexprTypeParameter extends Value {
    _name: string;
    constructor(origin: Origin, name: string, type: Type)
    {
        super();
        this._origin = origin;
        this._name = name;
        this._type = type;
    }

    get isConstexpr() {
        if (this.hasBecome) return this.target.isConstexpr;
        return true;
    }
    get isUnifiable() { return true; }
    get varIsLValue() { return false; }

    typeVariableUnify(unificationContext: UnificationContext, other: Value)
    {
        if (!(other instanceof Value))
            return false;

        if (!this.type.unify(unificationContext, other.type))
            return false;

        return this._typeVariableUnifyImpl(unificationContext, other);
    }

    unifyImpl(unificationContext: UnificationContext, other: Value)
    {
        return this.typeVariableUnify(unificationContext, other);
    }

    verifyAsArgument(unificationContext: UnificationContext): VerityResultType
    {
        return {result: true, reason: undefined};
    }

    verifyAsParameter(unificationContext: UnificationContext): VerityResultType
    {
        return {result: true, reason: undefined};
    }

    toString()
    {
        return this.type + " " + this.name;
    }
}

class Continue extends Node1 {
    constructor(origin: Origin)
    {
        super();
        this._origin = origin;
    }

    toString()
    {
        return "Continue";
    }
};

class ConvertPtrToArrayRefExpression extends Expression {
    _lValue: Node1;
    constructor(origin: Origin, lValue: Node1)
    {
        super(origin);
        this._lValue = lValue;
    }

    get lValue() { return this._lValue; }

    toString()
    {
        return "@(" + this.lValue + ")";
    }
}

class DereferenceExpression extends Expression {
    _ptr: Node1;
    constructor(origin: Origin, ptr: Node1, type:Type = null, addressSpace: string = null)
    {
        super(origin);
        this._ptr = ptr;
        this.type = type;
        this.addressSpace = addressSpace;
    }

    get ptr() { return this._ptr; }
    get isLValue() {
        if (this.hasBecome) return this.target.isLValue;
        return true;
    }

    toString()
    {
        return "*(" + this.ptr + ")";
    }
}

class DoWhileLoop extends Node1 {
    _body: Node1;
    _conditional: CallExpression | FunctionLikeBlock;
    constructor(origin: Origin, body: Node1, conditional: CallExpression)
    {
        super();
        this._origin = origin;
        this._body = body;
        this._conditional = conditional;
    }

    get body() { return this._body; }
    get conditional() { return this._conditional; }

    toString()
    {
        return "do " + this.body + " while (" + this.conditional + ");";
    }
}

class DotExpression extends PropertyAccessExpression {
    _fieldName: string;
    constructor(origin: Origin, struct1: Expression, fieldName: string)
    {
        super(origin, struct1);
        this._fieldName = fieldName;
    }

    get struct1() { return this.base; }
    get fieldName() { return this._fieldName; }

    get getFuncName() { return "operator." + this.fieldName; }
    get andFuncName() { return "operator&." + this.fieldName; }
    get setFuncName() { return "operator." + this.fieldName + "="; }

    toString()
    {
        return "(" + this.struct1 + "." + this.fieldName + ")";
    }
}

class EArrayRef {
    _ptr: EPtr;
    _length: int;
    constructor(ptr: EPtr, length: int)
    {
        if (ptr === undefined) {
            throw new Error("EArrayRef ptr is undefined");
        }
        this._ptr = ptr;
        this._length = length;
    }

    get ptr() { return this._ptr; }
    get length() { return this._length; }

    toString()
    {
        return "A:<" + this.ptr + ", " + this.length + ">";
    }
}

let eBufferCount: int = 0;
let canAllocateEBuffers: boolean = true;

class EBuffer {
    _index: int;
    _array: EPtrBoxValue[];
    constructor(size: int)
    {
        if (!canAllocateEBuffers)
            throw new Error("Trying to allocate EBuffer while allocation is disallowed");
        this._index = eBufferCount++;
        this._array = new Array(size);
    }

    static setCanAllocateEBuffers(value: boolean, callback: ()=>EPtr): EPtr
    {
        let oldCanAllocateEBuffers = canAllocateEBuffers;
        canAllocateEBuffers = value;
        try {
            return callback();
        } finally {
            canAllocateEBuffers = oldCanAllocateEBuffers;
        }
    }

    static disallowAllocation(callback: ()=>EPtr): EPtr
    {
        return EBuffer.setCanAllocateEBuffers(false, callback);
    }

    static allowAllocation(callback: ()=>EPtr)
    {
        return EBuffer.setCanAllocateEBuffers(true, callback);
    }

    get(index: int)
    {
        if (index < 0 || index >= this._array.length)
            throw new Error("Out of bounds buffer access (buffer = " + this + ", index = " + index + ")");
        return this._array[index];
    }

    set(index: int, value: EPtrBoxValue)
    {
        if (index < 0 || index >= this._array.length)
            throw new Error("out of bounds buffer access (buffer = " + this + ", index = " + index + ")");
        this._array[index] = value;
    }

    get index() { return this._index; }

    toString()
    {
        return "B" + this._index + ":[" + this._array + "]";
    }
}

class EBufferBuilder extends Visitor {
    _program: Program;
    constructor(program: Program)
    {
        super();
        this._program = program;
    }

    _createEPtr(type: Type)
    {
        if (type.size == null)
            throw new Error("Type does not have size: " + type);
        let buffer = new EBuffer(type.size);
        if (!type.populateDefaultValue)
            throw new Error("Cannot populateDefaultValue with: " + type);
        type.populateDefaultValue(buffer, 0);
        return new EPtr(buffer, 0);
    }

    _createEPtrForNode(node: Node1)
    {
        if (!node.type)
            throw new Error("node has no type: " + node);
        node.ePtr = this._createEPtr(node.type);
    }

    visitFuncParameter(node: FuncParameter)
    {
        this._createEPtrForNode(node);
    }

    visitVariableDecl(node: VariableDecl)
    {
        this._createEPtrForNode(node);
        if (node.initializer)
            node.initializer.visit(this);
    }

    visitFuncDef(node: FuncDef)
    {
        node.returnEPtr = this._createEPtr(node.returnType);
        super.visitFuncDef(node);
    }

    visitFunctionLikeBlock(node: FunctionLikeBlock)
    {
        node.returnEPtr = this._createEPtr(node.returnType);
        super.visitFunctionLikeBlock(node);
    }

    visitCallExpression(node: CallExpression)
    {
        node.resultEPtr = this._createEPtr(node.resultType);
        super.visitCallExpression(node);
    }

    visitMakePtrExpression(node: MakePtrExpression)
    {
        node.ePtr = EPtr.box();
        super.visitMakePtrExpression(node);
    }

    visitGenericLiteral(node: GenericLiteral)
    {
        node.ePtr = EPtr.box();
    }

    visitNullLiteral(node: NullLiteral)
    {
        node.ePtr = EPtr.box();
    }

    visitBoolLiteral(node: BoolLiteral)
    {
        node.ePtr = EPtr.box();
    }

    visitEnumLiteral(node: EnumLiteral)
    {
        node.ePtr = EPtr.box();
    }

    visitLogicalNot(node: LogicalNot)
    {
        node.ePtr = EPtr.box();
        super.visitLogicalNot(node);
    }

    visitLogicalExpression(node: LogicalExpression)
    {
        node.ePtr = EPtr.box();
        super.visitLogicalExpression(node);
    }

    visitAnonymousVariable(node: AnonymousVariable)
    {
        this._createEPtrForNode(node);
    }

    visitMakeArrayRefExpression(node: MakeArrayRefExpression)
    {
        node.ePtr = EPtr.box();
        super.visitMakeArrayRefExpression(node);
    }

    visitConvertPtrToArrayRefExpression(node: ConvertPtrToArrayRefExpression)
    {
        node.ePtr = EPtr.box();
        super.visitConvertPtrToArrayRefExpression(node);
    }
}

class EPtr {
    _buffer: EBuffer;
    _offset: int;
    constructor(buffer: EBuffer, offset: int)
    {
        if (offset == null || offset != offset)
            throw new Error("Bad offset: " + offset);
        this._buffer = buffer;
        this._offset = offset;
    }

    // The interpreter always passes around pointers to things. This means that sometimes we will
    // want to return a value but we have to "invent" a pointer to that value. No problem, this
    // function is here to help.
    //
    // In a real execution environment, uses of this manifest as SSA temporaries.
    static box(value?: EPtrBoxValue)
    {
        return new EPtr(new EBuffer(1), 0).box(value);
    }

    box(value: EPtrBoxValue)
    {
        this._buffer.set(0, value);
        return this;
    }

    get buffer() { return this._buffer; }
    get offset() { return this._offset; }

    plus(offset: int)
    {
        return new EPtr(this.buffer, this.offset + offset);
    }

    loadValue(): EPtrBoxValue
    {
        return this.buffer.get(this.offset);
    }

    get(offset: int): EPtrBoxValue
    {
        return this.buffer.get(this.offset + offset);
    }

    set(offset: int, value: EPtrBoxValue)
    {
        this.buffer.set(this.offset + offset, value);
    }

    copyFrom(other: EPtr, size: int)
    {
        if (size == null)
            throw new Error("Cannot copy null size");
        for (let i = size; i--;)
            this.set(i, other.get(i));
    }

    toString()
    {
        return "B" + this.buffer.index + ":" + this.offset;
    }
}

/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";

class EnumLiteral extends Expression {
    _member: EnumMember;
    constructor(origin: Origin, member: EnumMember)
    {
        super(origin);
        this._member = member;
        this.type = this.member.enumType;
    }

    get member() { return this._member; }
    // get type() { return this.member.enumType; }
    get isConstexpr() {
        if (this.hasBecome) return this.target.isConstexpr;
        return true;
    }

    unifyImpl(unificationContext: UnificationContext, other: EnumLiteral)
    {
        if (!(other instanceof EnumLiteral))
            return false;
        return this.member == other.member;
    }

    get valueForSelectedType()
    {
        return (<GenericLiteral>this.member.value.unifyNode).valueForSelectedType;
    }

    toString()
    {
        return this.member.enumType.name + "." + this.member.name;
    }
}

class EnumMember extends Node1 {
    value: Expression;
    _name: string;
    enumType: EnumType;
    constructor(origin: Origin, name: string, value: Expression)
    {
        super();
        this._origin = origin;
        this._name = name;
        this.value = value;
    }

    toString()
    {
        let result = this.name;
        if (this.value)
            result += " = " + this.value;
        return result;
    }
}

function EnumTypePopulate(buffer: EBuffer, offset: int) {
    this.baseType.populateDefaultValue(buffer, offset);
}

class EnumType extends Type {
    _name: string;
    _baseType: Type;
    _members: Map<string, EnumMember>;

    constructor(origin: Origin, name: string, baseType: Type)
    {
        super();
        this._origin = origin;
        this._name = name;
        this._baseType = baseType;
        this._members = new Map();
        this.allValues =  function*() {
            for (let member of this.members)
            yield {value: (<GenericLiteral>member.value.unifyNode).valueForSelectedType, name: member.name};
        }
        this.valuesEqual = (a: number, b: number)=>{return (<Type>this.baseType.unifyNode).valuesEqual(a, b)};
        this.populateDefaultValue = EnumTypePopulate;
    }

    add(member: EnumMember)
    {
        if (this._members.has(member.name))
            throw new WTypeError(member.origin.originString, "Duplicate enum member name: " + member.name);
        member.enumType = this;
        this._members.set(member.name, member);
    }

    get baseType() { return this._baseType; }

    get memberNames() { return this._members.keys(); }
    memberByName(name: string) { return this._members.get(name); }
    get members() { return this._members.values(); }
    get memberMap() { return this._members; }

    get isPrimitive() { return true; }
    get size() { return this.baseType.size; }

    toString()
    {
        return "enum " + this.name + " : " + this.baseType + " { " + Array.from(this.members).join(",") + " }";
    }
}

/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";

const BreakException = Symbol("BreakException");
const ContinueException = Symbol("ContinueException");


/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";

// This is a combined LHS/RHS evaluator that passes around EPtr's to everything.
class Evaluator extends Visitor {
    _program: Program
    constructor(program: Program)
    {
        super();
        this._program = program;
    }

    // You must snapshot if you use a value in rvalue context. For example, a call expression will
    // snapshot all of its arguments immedaitely upon executing them. In general, it should not be
    // possible for a pointer returned from a visit method in rvalue context to live across any effects.
    _snapshot(type: Type, dstPtr: EPtr, srcPtr: EPtr)
    {
        let size = type.size;
        if (size == null)
            throw new Error("Cannot get size of type: " + type + " (size = " + size + ", constructor = " + type.constructor.name + ")");
        if (!dstPtr)
            dstPtr = new EPtr(new EBuffer(size), 0);
        dstPtr.copyFrom(srcPtr, size);
        return dstPtr;
    }

    runFunc(func: FuncDef): EPtr
    {
        return EBuffer.disallowAllocation(
            () => this._runBody(func.returnType, func.returnEPtr, func.body));
    }

    _runBody(type: Type, ptr: EPtr, block: Node1)
    {
        if (!ptr)
            throw new Error("Null ptr");
        try {
            block.visit(this);
            // FIXME: We should have a check that there is no way to drop out of a function without
            // returning unless the function returns void.
            return null;
        } catch (e) {
            if (e == BreakException || e == ContinueException)
                throw new Error("Should not see break/continue at function scope");
            if (e instanceof ReturnException) {
                let result = this._snapshot(type, ptr, <EPtr><unknown>e.value);
                return result;
            }
            throw e;
        }
    }

    visitFunctionLikeBlock(node: FunctionLikeBlock)
    {
        for (let i = 0; i < node.argumentList.length; ++i) {
            node.parameters[i].ePtr.copyFrom(
                <EPtr>node.argumentList[i].visit(this),
                node.parameters[i].type.size);
        }
        let result = this._runBody(node.returnType, node.returnEPtr, node.body);
        return result;
    }

    visitReturn(node: Return)
    {
        throw new ReturnException(node.value ? <Value>node.value.visit(this) : null);
    }

    visitVariableDecl(node: VariableDecl)
    {
        if (!node.ePtr.buffer)
            throw new Error("eptr without buffer in " + node);
        node.type.populateDefaultValue(node.ePtr.buffer, node.ePtr.offset);
        if (node.initializer)
            node.ePtr.copyFrom(<EPtr>node.initializer.visit(this), node.type.size);
    }

    visitAssignment(node: Assignment)
    {
        let target = <EPtr>node.lhs.visit(this);
        let source = <EPtr>node.rhs.visit(this);
        target.copyFrom(source, node.type.size);
        return target;
    }

    visitIdentityExpression(node: Node1)
    {
        return node.target.visit(this);
    }

    visitDereferenceExpression(node: DereferenceExpression)
    {
        let ptr = <Node1>(<EPtr>node.ptr.visit(this)).loadValue();
        if (!ptr)
            throw new WTrapError(node.origin.originString, "Null dereference");
        return ptr;
    }

    visitMakePtrExpression(node: MakePtrExpression)
    {
        let ptr = <Value>node.lValue.visit(this);
        return node.ePtr.box(ptr);
    }

    visitMakeArrayRefExpression(node: MakeArrayRefExpression)
    {
        node.lValue.constructor.name;
        return node.ePtr.box(new EArrayRef(<EPtr>node.lValue.visit(this), <int>(<EPtr>node.numElements.visit(this)).loadValue()));
    }

    visitConvertPtrToArrayRefExpression(node: ConvertPtrToArrayRefExpression)
    {
        return node.ePtr.box(new EArrayRef(<EPtr>(<EPtr>node.lValue.visit(this)).loadValue(), 1));
    }

    visitCommaExpression(node: CommaExpression)
    {
        let result: Node1;
        for (let expression of node.list)
            result = <Node1>(<Node1>expression).visit(this);
        // This should almost snapshot, except that tail-returning a pointer is totally OK.
        return result;
    }

    visitVariableRef(node: VariableRef)
    {
        return node.variable.ePtr;
    }

    visitGenericLiteral(node: GenericLiteral)
    {
        return node.ePtr.box(node.valueForSelectedType);
    }

    visitNullLiteral(node: NullLiteral)
    {
        return node.ePtr.box(null);
    }

    visitBoolLiteral(node: BoolLiteral)
    {
        return node.ePtr.box(node.value);
    }

    visitEnumLiteral(node: EnumLiteral)
    {
        return node.ePtr.box((<GenericLiteral>node.member.value.unifyNode).valueForSelectedType);
    }

    visitLogicalNot(node: LogicalNot)
    {
        let result = !(<EPtr>node.operand.visit(this)).loadValue();
        return node.ePtr.box(result);
    }

    visitLogicalExpression(node: LogicalExpression)
    {
        let lhs = <number>((<EPtr>node.left.visit(this)).loadValue());
        let rhs = <number>((<EPtr>node.right.visit(this)).loadValue());
        let result: number;
        switch (node.text) {
        case "&&":
            result = lhs && rhs;
            break;
        case "||":
            result = lhs || rhs;
            break;
        default:
            throw new Error("Unknown type of logical expression");
        }
        return node.ePtr.box(result);
    }

    visitIfStatement(node: IfStatement)
    {
        if ((<EPtr>node.conditional.visit(this)).loadValue())
            return node.body.visit(this);
        else if (node.elseBody)
            return node.elseBody.visit(this);
    }

    visitWhileLoop(node: WhileLoop)
    {
        while ((<EPtr>node.conditional.visit(this)).loadValue()) {
            try {
                node.body.visit(this);
            } catch (e) {
                if (e == BreakException)
                    break;
                if (e == ContinueException)
                    continue;
                throw e;
            }
        }
    }

    visitDoWhileLoop(node: DoWhileLoop)
    {
        do {
            try {
                node.body.visit(this);
            } catch (e) {
                if (e == BreakException)
                    break;
                if (e == ContinueException)
                    continue;
                throw e;
            }
        } while ((<EPtr>node.conditional.visit(this)).loadValue());
    }

    visitForLoop(node: ForLoop)
    {
        for (node.initialization ? node.initialization.visit(this) : true;
            node.condition ? (<EPtr>node.condition.visit(this)).loadValue() : true;
            node.increment ? node.increment.visit(this) : true) {
            try {
                node.body.visit(this);
            } catch (e) {
                if (e == BreakException)
                    break;
                if (e == ContinueException)
                    continue;
                throw e;
            }
        }
    }

    visitSwitchStatement(node: SwitchStatement)
    {
        type Predicate = (switchCase: SwitchCase) => boolean;
        let findAndRunCast = (predicate: Predicate) => {
            for (let i = 0; i < node.switchCases.length; ++i) {
                let switchCase = node.switchCases[i];
                if (predicate(switchCase)) {
                    try {
                        for (let j = i; j < node.switchCases.length; ++j)
                            node.switchCases[j].visit(this);
                    } catch (e) {
                        if (e != BreakException)
                            throw e;
                    }
                    return true;
                }
            }
            return false;
        };

        let value = <EPtrBoxValue>(<EPtr>node.value.visit(this)).loadValue();

        let found = findAndRunCast(switchCase => {
            if (switchCase.isDefault)
                return false;
            return (<Type>node.type.unifyNode).valuesEqual(
                <number>value, (<EnumLiteral|IntLiteral>switchCase.value.unifyNode).valueForSelectedType);
        });
        if (found)
            return;

        found = findAndRunCast(switchCase => switchCase.isDefault);
        if (!found)
            throw new Error("Switch statement did not find case");
    }

    visitBreak(node: Break)
    {
        throw BreakException;
    }

    visitContinue(node: Continue)
    {
        throw ContinueException;
    }

    visitTrapStatement(node: TrapStatement)
    {
        throw new WTrapError(node.origin.originString, "Trap statement");
    }

    visitAnonymousVariable(node: AnonymousVariable)
    {
        node.type.populateDefaultValue(node.ePtr.buffer, node.ePtr.offset);
    }

    visitCallExpression(node: CallExpression)
    {
        // We evaluate inlined ASTs, so this can only be a native call.
        let callArguments: (()=> EPtr)[] = [];
        for (let i = 0; i < node.argumentList.length; ++i) {
            let argument = node.argumentList[i];
            let type = node.nativeFuncInstance.parameterTypes[i];
            if (!type || !argument)
                throw new Error("Cannot get type or argument; i = " + i + ", argument = " + argument + ", type = " + type + "; in " + node);
            let argumentValue = <EPtr>argument.visit(this);
            if (!argumentValue)
                throw new Error("Null argument value, i = " + i + ", node = " + node);
            callArguments.push(() => {
                let result = this._snapshot(type, null, argumentValue);
                return result;
            });
        }

        // For simplicity, we allow intrinsics to just allocate new buffers, and we allocate new
        // buffers when snapshotting their arguments. This is not observable to the user, so it's OK.
        let result = <EPtr>EBuffer.allowAllocation(
            () => node.func.implementation(callArguments.map(
                (thunk: Function) => { let thunkFunc = thunk; return thunkFunc()}),
            node));

        result = this._snapshot(node.nativeFuncInstance.returnType, node.resultEPtr, result);
        return result;
    }
}

class ExpressionFinder extends Visitor {
    _callback: (node: Node1)=>void;
    constructor(callback: (node: Node1)=>void)
    {
        super();
        this._callback = callback;
    }

    visitFunc(node: Func)
    {
        this._callback(node.returnType);
        for (let typeParameter of node.typeParameters)
            typeParameter.visit(this);
        for (let parameter of node.parameters)
            parameter.visit(this);
    }

    visitFuncParameter(node: FuncParameter)
    {
        this._callback(node.type);
    }

    visitConstexprTypeParameter(node: ConstexprTypeParameter)
    {
        this._callback(node.type);
    }

    visitAssignment(node: Assignment)
    {
        this._callback(node);
    }

    visitReadModifyWriteExpression(node: ReadModifyWriteExpression)
    {
        this._callback(node);
    }

    visitIdentityExpression(node: Node1)
    {
        this._callback(node);
    }

    visitCallExpression(node: CallExpression)
    {
        this._callback(node);
    }

    visitReturn(node: Return)
    {
        if (node.value)
            this._callback(node.value);
    }

    visitWhileLoop(node: WhileLoop)
    {
        this._callback(node.conditional);
        node.body.visit(this);
    }

    visitDoWhileLoop(node: DoWhileLoop)
    {
        node.body.visit(this);
        this._callback(node.conditional);
    }

    visitIfStatement(node: IfStatement)
    {
        this._callback(node.conditional);
        node.body.visit(this);
        if (node.elseBody)
            node.elseBody.visit(this);
    }

    visitForLoop(node: ForLoop)
    {
        // Initialization is a statement, not an expression. If it's an assignment or variableDecl, we'll
        // catch it and redirect to the callback.
        if (node.initialization)
            node.initialization.visit(this);

        if (node.condition)
            this._callback(node.condition);
        if (node.increment)
            this._callback(node.increment);

        node.body.visit(this);
    }

    visitVariableDecl(node: VariableDecl)
    {
        this._callback(node.type);
        if (node.initializer)
            this._callback(node.initializer);
    }
}

class ExternalOrigin {
    originString: string;
    originKind: string;
    constructor(originString: string, originKind: string) {
        this.originString = originString;
        this.originKind = originKind;
    }
}

const externalOrigin = new ExternalOrigin("<external>", "user");

class Field extends Node1 {
    _name: string;
    offset: int;
    struct: StructType;
    constructor(origin: Origin, name: string, type: Type)
    {
        super();
        this._origin = origin;
        this._name = name;
        this._type = type;
    }

    toString()
    {
        return this.type + " " + this.name;
    }
}

function findHighZombies(program: Program)
{
    program.visit(new HighZombieFinder());
}

function flattenProtocolExtends(program: Program)
{
    let visiting = new VisitingSet();

    function flatten(protocol: ProtocolDecl)
    {
        if (!protocol.extends.length)
            return;

        visiting.doVisit(protocol, () => {
            for (let parent of protocol.extends) {
                let parentDecl = parent.protocolDecl;
                flattenFunc(parentDecl);
                for (let signature of parentDecl.signatures) {
                    let newSignature = <ProtocolFuncDecl>signature.visit(
                        new Substitution([parentDecl.typeVariable], [protocol.typeVariable]));
                    protocol.add(newSignature);
                }
            }
            protocol.extends = [];
            return undefined;
        });
    }
    let flattenFunc: Function = flatten;

    for (let protocol of program.protocols.values())
        flattenFunc(<ProtocolDecl>protocol);
}
/*
class FlattenedStructOffsetGatherer extends Visitor {
    constructor(initial)
    {
        super();
        this._offset = 0;
        this._name = [initial];
        this._result = [];
    }

    get result()
    {
        return this._result;
    }

    visitReferenceType(node)
    {
    }

    visitField(node)
    {
        this._offset += node.offset;
        this._name.push(node.name);
        super.visitField(node);
        this._name.pop();
        this._offset -= node.offset;
    }

    visitNativeType(node)
    {
        this._result.push({name: this._name.join("."), offset: this._offset, type: node.name});
        super.visitNativeType(node);
    }

    visitTypeRef(node)
    {
        super.visitTypeRef(node);
        Node1.visit(node.type, this);
    }
}
*/

function foldConstexprs(program: Program)
{
    program.visit(new ConstexprFolder());
}

class ForLoop extends Node1 {
    _initialization: Expression;
    _condition: CallExpression | FunctionLikeBlock;
    _increment: Expression;
    _body: Node1;
    constructor(origin: Origin, initialization: Expression, condition: CallExpression, increment: Expression, body: Node1)
    {
        super();
        this._origin = origin;
        this._initialization = initialization;
        this._condition = condition;
        this._increment = increment;
        this._body = body;
    }

    get initialization() { return this._initialization; }
    get condition() { return this._condition; }
    get increment() { return this._increment; }
    get body() { return this._body; }

    toString()
    {
        return "for (" + (this.initialization ? this.initialization : " ") + "; " + (this.condition ? this.condition : "") + "; " + (this.increment ? this.increment : "") + ") " + this.body;
    }
};

class Func extends Node1 {
    _origin: Origin;
    _typeParameters: Node1[];
    _parameters: FuncParameter[];
    uninstantiatedReturnType: Type;
    _name: string;
    _returnType: Type;
    _isCast: boolean;
    _shaderType: string;
    implementation: (value: (Node1 | EPtr)[], node?: Node1) => EPtr;
    inlined: boolean;
    isRestricted: boolean;
    constructor(origin: Origin, name: string, returnType: Type, typeParameters: Node1[], parameters: FuncParameter[], isCast: boolean, shaderType: string)
    {
        if (!(origin instanceof LexerToken))
            throw new Error("Bad origin: " + origin);
        for (let parameter of parameters) {
            if (!parameter)
                throw new Error("Null parameter");
            if (!parameter.type)
                throw new Error("Null parameter type");
        }
        super();
        this._origin = origin;
        this._name = name;
        this._returnType = returnType;
        this._typeParameters = typeParameters;
        this._parameters = parameters;
        this._isCast = isCast;
        this._shaderType = shaderType;
    }

    get returnType() { return this._returnType; }
    get typeParameters() { return this._typeParameters; }
    get typeParametersForCallResolution() { return this.typeParameters; }
    get parameters() { return this._parameters; }
    get parameterTypes() { return this.parameters.map(parameter => parameter.type); }
    get isCast() { return this._isCast; }
    get shaderType() { return this._shaderType; }
    get returnTypeForOverloadResolution() { return this.isCast ? this.returnType : null; }

    get kind() { return Func; }

    toDeclString()
    {
        let result = "";
        if (this.shaderType)
            result += this.shaderType + " ";
        if (this.isCast)
            result += "operator<" + this.typeParameters + "> " + this.returnType;
        else
            result += this.returnType + " " + this.name + "<" + this.typeParameters + ">";
        return result + "(" + this.parameters + ")";
    }

    toString()
    {
        return this.toDeclString();
    }
}

class FuncDef extends Func {
    returnEPtr: EPtr;
    _body: Node1;
    isRestricted: boolean;
    constructor(origin: Origin, name: string, returnType: Type, typeParameters: Node1[], parameters: FuncParameter[], body: Node1, isCast: boolean, shaderType: string)
    {
        super(origin, name, returnType, typeParameters, parameters, isCast, shaderType);
        this._body = body;
        this.isRestricted = false;
    }

    get body() { return this._body; }

    rewrite(rewriter: Rewriter)
    {
        if (this._typeParameters.length)
            throw new Error("Cannot rewrite an uninstantiated function");
        this._returnType = <Type>this._returnType.visit(rewriter);
        this._parameters = <FuncParameter[]>this._parameters.map(parameter => parameter.visit(rewriter));
        this._body = <Node1>this.body.visit(rewriter);
    }

    toString()
    {
        return super.toString() + " " + this.body;
    }
}

class FindTypeVariable extends Visitor {
    func: Func;
    typeArguments: Node1[];
    constructor(func: Func, typeArguments: Node1[]) {
        super();
        this.func = func;
        this.typeArguments = typeArguments;
    }

    visitTypeRef(node: TypeRef)
    {
        for (let typeArgument of node.typeArguments)
            typeArgument.visit(this);
    }

    visitTypeVariable(node: TypeVariable) {
        throw new Error("Unexpected type variable: " + node + " when instantiating " + this.func + " with arguments " + this.typeArguments);
    }
}

class Instantiate {
    substitution: InstantiationSubstitution;
    instantiateImmediates: InstantiationInstantiateImmediates;
    constructor(substitution: InstantiationSubstitution, instantiateImmediates: InstantiationInstantiateImmediates) {
        this.substitution = substitution;
        this.instantiateImmediates = instantiateImmediates;
    }

    visitFuncDef(func: FuncDef)
    {
        let returnType = <Type>func.returnType.visit(this.substitution);
        returnType = <Type>returnType.visit(this.instantiateImmediates);
        let parameters = <FuncParameter[]>func.parameters.map(parameter => parameter.visit(this.substitution));
        parameters = <FuncParameter[]>parameters.map(parameter => parameter.visit(this.instantiateImmediates));
        let body = <Node1>func.body.visit(this.substitution);
        body = <Node1>body.visit(this.instantiateImmediates);
        return new FuncDef(
            func.origin, func.name, returnType, [], parameters, body, func.isCast,
            func.shaderType);
    }

    visitNativeFunc(func: NativeFunc)
    {
        return new NativeFuncInstance(
            func,
            <Type>(<Type>func.returnType.visit(this.substitution)).visit(this.instantiateImmediates),
            <FuncParameter[]>func.parameters.map(parameter => (<FuncParameter>parameter.visit(this.substitution)).visit(this.instantiateImmediates)),
            func.isCast,
            func.shaderType,
            func.instantiateImplementation(this.substitution));
    }
}

class FuncInstantiator {
    _program: Program;
    _instances: Map<Func, FuncInstantiatorInstances[]>
    constructor(program: Program)
    {
        this._program = program;
        this._instances = new Map();
    }

    // Returns a Func object that uniquely identifies a particular system of type arguments. You must
    // intantiate things with concrete types, because this code casually assumes this. Note that this
    // will return a different func from `func` no matter what. This ensures that we can use the
    // returned func for rewrites that instantiate types without destroying our ability to do overload
    // resolutions on the original Program.
    getUnique(func: Func, typeArguments: Node1[])
    {
        for (let typeArgument of typeArguments)
            typeArgument.visit(new FindTypeVariable(func, typeArguments));
        let instances = this._instances.get(func);
        if (!instances)
            this._instances.set(func, instances = []);
        for (let instance of instances) {
            let ok = true;
            for (let i = instance.typeArguments.length; i--;) {
                if (!instance.typeArguments[i].equals(typeArguments[i])) {
                    ok = false;
                    break;
                }
            }
            if (!ok)
                continue;
            return instance.func;
        }

        let thisInstantiator = this;
        let substitution = new InstantiationSubstitution(this, func.typeParameters, typeArguments);
        let instantiateImmediates = new InstantiationInstantiateImmediates();
        let resultingFunc = <Func>func.visit(new Instantiate(substitution, instantiateImmediates));
        resultingFunc.uninstantiatedReturnType = <Type>func.returnType.visit(substitution);
        let instance = {func: resultingFunc, typeArguments};
        instances.push(instance);
        return resultingFunc;
    }
}

class FuncParameter extends Value {
    // The name is optional. It's OK for it to be null!
    constructor(origin: Origin, name: string, type: Type)
    {
        super();
        this._origin = origin;
        this._name = name;
        this._type = type;
    }

    get varIsLValue() { return true; }

    toString()
    {
        if (!this.name)
            return "" + this.type;
        return this.type + " " + this.name;
    }
}

class FunctionLikeBlock extends Value {
    returnEPtr: EPtr;
    resultType: Type;
    _returnType: Type;
    _argumentList: Node1[];
    _parameters: FuncParameter[];
    _body: Node1;
    isCast: boolean;
    constructor(origin: Origin, returnType: Type, argumentList:Node1[], parameters: FuncParameter[], body: Node1)
    {
        super();
        this._origin = origin;
        this._returnType = returnType;
        this._argumentList = argumentList;
        this._parameters = parameters;
        this._body = body;
    }

    get returnType() { return this._returnType; }
    get argumentList() { return this._argumentList; }
    get parameters() { return this._parameters; }
    get body() { return this._body; }

    toString()
    {
        return "([&] (" + this.parameters + ") -> " + this.returnType + " { " + this.body + " }(" + this.argumentList + "))";
    }
}

class HighZombieFinder extends Visitor {
    _found(node: Node1)
    {
        throw new Error(node._origin.originString + ": High zombie: " + node);
    }

    visitDotExpression(node: DotExpression)
    {
        this._found(node);
    }

    visitIndexExpression(node: IndexExpression)
    {
        this._found(node);
    }

    visitCallExpression(node: CallExpression)
    {
        super.visitCallExpression(node);
    }
}

class IdentityExpression extends Expression {
    constructor(target: Node1)
    {
        super(target.origin);
        this._target = target;
    }

    get unifyNode() { return this.target.unifyNode; }
    get isConstexpr() { return this.target.isConstexpr; }
    get isLValue() { return this.target.isLValue; }

    get addressSpace() { return this.target.addressSpace; }

    toString()
    {
        return "(" + this.target + ")";
    }
}

class IfStatement extends Node1 {
    _conditional: CallExpression | FunctionLikeBlock;
    _body: Node1;
    _elseBody: Node1;
    constructor(origin: Origin, conditional: CallExpression, body: Node1, elseBody: Node1)
    {
        super();
        this._origin = origin;
        this._conditional = conditional;
        this._body = body;
        this._elseBody = elseBody;
    }

    get conditional() { return this._conditional; }
    get body() { return this._body; }
    get elseBody() { return this._elseBody; }

    toString()
    {
        let result = "if (" + this.conditional + ") " + this.body;
        if (this.elseBody)
            return result + " else " + this.elseBody;
        return result;
    }
};

class IndexExpression extends PropertyAccessExpression {
    _index: Expression;
    constructor(origin: Origin, array: Expression, index: Expression)
    {
        super(origin, array);
        this._index = index;
    }

    get array() { return this.base; }
    get index() { return this._index; }

    get getFuncName() { return "operator[]"; }
    get andFuncName() { return "operator&[]"; }
    get setFuncName() { return "operator[]="; }

    updateCalls()
    {
        if (this.callForGet)
            this.callForGet.argumentList[1] = this.index;
        if (this.callForAnd)
            this.callForAnd.argumentList[1] = this.index;
        if (this.callForSet)
            this.callForSet.argumentList[1] = this.index;
        super.updateCalls();
    }

    toString()
    {
        return "(" + this.array + "[" + this.index + "])";
    }
}

function inferTypesForCall(func: Func, typeArguments: Node1[], argumentTypes: Type[], returnType: Type): InferTypeRet
{
    if (typeArguments.length && typeArguments.length != func.typeParameters.length)
        return {failure: new OverloadResolutionFailure(func, "Wrong number of type arguments (passed " + typeArguments.length + ", require " + func.typeParameters.length + ")"), func: undefined};
    if (argumentTypes.length != func.parameters.length)
        return {failure: new OverloadResolutionFailure(func, "Wrong number of arguments (passed " + argumentTypes.length + ", require " + func.parameters.length + ")"), func: undefined};
    let unificationContext = new UnificationContext(func.typeParametersForCallResolution);
    for (let i = 0; i < typeArguments.length; ++i) {
        let argument = typeArguments[i];
        let parameter = func.typeParameters[i];
        if (!argument.unify(unificationContext, parameter))
            return {failure: new OverloadResolutionFailure(func, "Type argument #" + (i + 1) + " for parameter " + parameter.name + " does not match (passed " + argument + ", require " + parameter + ")"), func: undefined};
    }
    for (let i = 0; i < argumentTypes.length; ++i) {
        if (!argumentTypes[i])
            throw new Error("Null argument type at i = " + i);
        if (!argumentTypes[i].unify(unificationContext, func.parameters[i].type))
            return {failure: new OverloadResolutionFailure(func, "Argument #" + (i + 1) + " " + (func.parameters[i].name ? "for parameter " + func.parameters[i].name + " " : "") + "does not match (passed " + argumentTypes[i] + ", require " + func.parameters[i].type + ")"), func: undefined};
    }
    if (returnType && !returnType.unify(unificationContext, func.returnType))
        return {failure: new OverloadResolutionFailure(func, "Return type " + func.returnType + " does not match " + returnType), func: undefined};
    let verificationResult = unificationContext.verify();
    if (!verificationResult.result)
        return {failure: new OverloadResolutionFailure(func, verificationResult.reason), func: undefined};
    let shouldBuildTypeArguments = !typeArguments.length;
    if (shouldBuildTypeArguments)
        typeArguments = [];
    for (let typeParameter of func.typeParameters) {
        let typeArgument = <Type>unificationContext.find(typeParameter);
        if (typeArgument == typeParameter)
            return {failure: new OverloadResolutionFailure(func, "Type parameter " + typeParameter + " did not get assigned a type"), func: undefined};
        if (shouldBuildTypeArguments)
            typeArguments.push(typeArgument);
    }
    return {failure: undefined, func, unificationContext, typeArguments};
}

function inline(program: Program)
{
    for (let funcList of program.functions.values()) {
        for (let func of funcList) {
            if (!func.typeParameters.length) {
                func = program.funcInstantiator.getUnique(func, [])
                _inlineFunction(program, func, new VisitingSet(func));
            }
        }
    }
}

function _inlineFunction(program: Program, func: Func, visiting: VisitingSet)
{
    if (func.typeParameters.length)
        throw new Error("Cannot inline function that has type parameters");
    if (func.inlined || func.isNative)
        return;

    func.visit(new LateChecker());

    // This is the precise time when we can build EBuffers in order to get them to be uniqued by
    // type instantiation but nothing else.
    func.visit(new StructLayoutBuilder());
    func.visit(new EBufferBuilder(program));

    (<FuncDef>func).rewrite(new Inliner(program, func, visiting));

    func.inlined = true;
}

function resolveInlinedFunction(program: Program, name: string, typeArguments: Type[], argumentTypes: Type[], allowEntryPoint = false)
{
    let overload = <OverLoad>program.globalNameContext.resolveFuncOverload(name, typeArguments, argumentTypes, undefined, allowEntryPoint);
    if (!overload.func)
        return (<OverLoadFailure>overload).failures;
    if (!overload.func.typeParameters)
        return overload.func;
    let func = program.funcInstantiator.getUnique(overload.func, (<OverLoadSuccess>overload).typeArguments);
    _inlineFunction(program, func, new VisitingSet(overload.func));
    return func;
}

class Inliner extends Rewriter {
    _program: Program;
    _visiting: VisitingSet;
    constructor(program: Program, func: Func, visiting: VisitingSet)
    {
        super();
        this._program = program;
        this._visiting = visiting;
    }

    visitCallExpression(node: CallExpression): Node1
    {
        let result = <CallExpression>super.visitCallExpression(node);
        if (result.nativeFuncInstance)
            return result;
        return <Node1>this._visiting.doVisit(node.func, () => {
            let func = this._program.funcInstantiator.getUnique(result.func, result.actualTypeArguments);
            if (func.isNative)
                throw new Error("Unexpected native func: " + func);
            _inlineFunction(this._program, func, this._visiting);
            let resultingBlock = new FunctionLikeBlock(
                result.origin, func.returnType, result.argumentList, func.parameters, (<FuncDef>func).body);
            resultingBlock.returnEPtr = result.resultEPtr;
            return resultingBlock;
        });
    }
}

class InstantiateImmediates extends Rewriter {
    visitTypeRef(node: TypeRef)
    {
        node = <TypeRef>super.visitTypeRef(node);
        if (!node.type.instantiate) {
            if (node.typeArguments.length)
                throw new Error("type does not support instantiation: " + node.type + " (" + node.type.constructor.name + ")");
            return node;
        }
        return node.type.instantiate(node.typeArguments).visit(new AutoWrapper());
    }

    visitReferenceType(node: ReferenceType)
    {
        return node;
    }
}

class InstantiationInstantiateImmediates extends InstantiateImmediates {
    visitCallExpression(node: CallExpression)
    {
        // We need to preserve certain things that would have instantiated, but that we cannot
        // instantiate without breaking chain-instantiations (generic function calls generic
        // function so therefore the instantiated generic function must still have the original
        // (uninstantiated) types to instantiate the generic function that it calls).
        let result: CallExpression = new CallExpression(
            node.origin, node.name, node.typeArguments,
            <Node1[]>node.argumentList.map((argument: Node1) => Node1.visit(argument, this)));
        result = this.processDerivedCallData(node, result);

        result.argumentTypes = Array.from(node.argumentTypes);
        if (node.isCast)
            result.setCastData(node.returnType);
        result.actualTypeArguments = Array.from(node.actualTypeArguments);

        return result;
    }
}

class Intrinsics {
    bool: Type;
    int32: Type;
    uint32: Type;
    uint8: Type;
    float: Type;
    double: Type;
    void: Type;
    _map: Map<string, (type:Type|Func)=>void>;
    constructor(nameContext: NameContext)
    {
        this._map = new Map();

        // NOTE: Intrinsic resolution happens before type name resolution, so the strings we use here
        // to catch the intrinsics must be based on the type names that StandardLibraryPrologue.js uses.
        // For example, if a native function is declared using "int" rather than "int32", then we must
        // use "int" here, since we don't yet know that they are the same type.

        this._map.set(
            "native typedef void<>",
            type => {
                type = <Type>(type);
                this.void = type;
                type.size = 0;
                type.populateDefaultValue = () => { };
            });

        function isBitwiseEquivalent(left: number, right: number)
        {
            let doubleArray = new Float64Array(1);
            let intArray = new Int32Array(doubleArray.buffer);
            doubleArray[0] = left;
            let leftInts = Int32Array.from(intArray);
            doubleArray[0] = right;
            for (let i = 0; i < 2; ++i) {
                if (leftInts[i] != intArray[i])
                    return false;
            }
            return true;
        }

        this._map.set(
            "native typedef int32<>",
            (type: Type) => {
                this.int32 = type;
                type.isPrimitive = true;
                type.isInt = true;
                type.isNumber = true;
                type.isSigned = true;
                type.canRepresent = value => isBitwiseEquivalent(value | 0, value);
                type.size = 1;
                type.defaultValue = 0;
                type.createLiteral = (origin, value) => IntLiteral.withType(origin, value | 0, type);
                type.successorValue = value => (value + 1) | 0;
                type.valuesEqual = (a, b) => a === b;
                type.populateDefaultValue = (buffer, offset) => buffer.set(offset, 0);
                type.formatValueFromIntLiteral = value => value | 0;
                type.formatValueFromUintLiteral = value => value | 0;
                type.allValues = function*() {
                    for (let i = 0; i <= 0xffffffff; ++i) {
                        let value = i | 0;
                        yield {value: value, name: value};
                    }
                };
            });

        this._map.set(
            "native typedef uint32<>",
            (type: Type) => {
                this.uint32 = type;
                type.isPrimitive = true;
                type.isInt = true;
                type.isNumber = true;
                type.isSigned = false;
                type.canRepresent = value => isBitwiseEquivalent(value >>> 0, value);
                type.size = 1;
                type.defaultValue = 0;
                type.createLiteral = (origin, value) => IntLiteral.withType(origin, value >>> 0, type);
                type.successorValue = value => (value + 1) >>> 0;
                type.valuesEqual = (a, b) => a === b;
                type.populateDefaultValue = (buffer, offset) => buffer.set(offset, 0);
                type.formatValueFromIntLiteral = value => value >>> 0;
                type.formatValueFromUintLiteral = value => value >>> 0;
                type.allValues = function*() {
                    for (let i = 0; i <= 0xffffffff; ++i)
                        yield {value: i, name: i};
                };
            });

        this._map.set(
            "native typedef uint8<>",
            (type: Type) => {
                this.uint8 = type;
                type.isInt = true;
                type.isNumber = true;
                type.isSigned = false;
                type.canRepresent = value => isBitwiseEquivalent(value & 0xff, value);
                type.size = 1;
                type.defaultValue = 0;
                type.createLiteral = (origin, value) => IntLiteral.withType(origin, value & 0xff, type);
                type.successorValue = value => (value + 1) & 0xff;
                type.valuesEqual = (a, b) => a === b;
                type.populateDefaultValue = (buffer, offset) => buffer.set(offset, 0);
                type.formatValueFromIntLiteral = value => value & 0xff;
                type.formatValueFromUintLiteral = value => value & 0xff;
                type.allValues = function*() {
                    for (let i = 0; i <= 0xff; ++i)
                        yield {value: i, name: i};
                };
            });

        this._map.set(
            "native typedef float32<>",
            (type: Type) => {
                this.float = type;
                type.isPrimitive = true;
                type.size = 1;
                type.isFloating = true;
                type.isNumber = true;
                type.canRepresent = value => isBitwiseEquivalent(Math.fround(value), value);
                type.populateDefaultValue = (buffer, offset) => buffer.set(offset, 0);
                type.formatValueFromIntLiteral = value => value;
                type.formatValueFromUintLiteral = value => value;
                type.formatValueFromFloatLiteral = (value: number) => Math.fround(value);
                type.formatValueFromDoubleLiteral = (value: number) => Math.fround(value);
            });

        this._map.set(
            "native typedef float64<>",
            (type: Type) => {
                this.double = type;
                type.isPrimitive = true;
                type.size = 1;
                type.isFloating = true;
                type.isNumber = true;
                type.canRepresent = value => true;
                type.populateDefaultValue = (buffer, offset) => buffer.set(offset, 0);
                type.formatValueFromIntLiteral = value => value;
                type.formatValueFromUintLiteral = value => value;
                type.formatValueFromFloatLiteral = value => value;
                type.formatValueFromDoubleLiteral = value => value;
            });

        this._map.set(
            "native typedef bool<>",
            type => {
                type = <Type>(type);
                this.bool = type;
                type.isPrimitive = true;
                type.size = 1;
                type.populateDefaultValue = (buffer, offset) => buffer.set(offset, false);
            });

        this._map.set(
            "native operator<> int32(uint32)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box(<number>(<EPtr>value).loadValue() | 0);
            });

        this._map.set(
            "native operator<> int32(uint8)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box(<number>(<EPtr>value).loadValue() | 0);
            });

        this._map.set(
            "native operator<> int32(float)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box(<number>(<EPtr>value).loadValue() | 0);
            });

        this._map.set(
            "native operator<> int32(double)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box(<number>(<EPtr>value).loadValue() | 0);
            });

        this._map.set(
            "native operator<> uint32(int32)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box(<number>(<EPtr>value).loadValue() >>> 0);
            });

        this._map.set(
            "native operator<> uint32(uint8)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box(<number>(<EPtr>value).loadValue() >>> 0);
            });

        this._map.set(
            "native operator<> uint32(float)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box(<number>(<EPtr>value).loadValue() >>> 0);
            });

        this._map.set(
            "native operator<> uint32(double)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box(<number>(<EPtr>value).loadValue() >>> 0);
            });

        this._map.set(
            "native operator<> uint8(int32)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box(<number>(<EPtr>value).loadValue() & 0xff);
            });

        this._map.set(
            "native operator<> uint8(uint32)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box(<number>(<EPtr>value).loadValue() & 0xff);
            });

        this._map.set(
            "native operator<> uint8(float)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box(<number>(<EPtr>value).loadValue() & 0xff);
            });

        this._map.set(
            "native operator<> uint8(double)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box(<number>(<EPtr>value).loadValue() & 0xff);
            });

        this._map.set(
            "native operator<> float(double)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box(Math.fround(<number>(<EPtr>value).loadValue()));
            });

        this._map.set(
            "native operator<> float(int32)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box(Math.fround(<number>(<EPtr>value).loadValue()));
            });

        this._map.set(
            "native operator<> float(uint32)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box(Math.fround(<number>(<EPtr>value).loadValue()));
            });

        this._map.set(
            "native operator<> float(uint8)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box(Math.fround(<number>(<EPtr>value).loadValue()));
            });

        this._map.set(
            "native operator<> double(float)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box((<EPtr>value).loadValue());
            });

        this._map.set(
            "native operator<> double(int32)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box((<EPtr>value).loadValue());
            });

        this._map.set(
            "native operator<> double(uint32)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box((<EPtr>value).loadValue());
            });

        this._map.set(
            "native operator<> double(uint8)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box((<EPtr>value).loadValue());
            });

        this._map.set(
            "native int operator+<>(int,int)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<number>(<EPtr>left).loadValue() + <number>(<EPtr>right).loadValue()) | 0);
            });

        this._map.set(
            "native uint operator+<>(uint,uint)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<number>(<EPtr>left).loadValue() + <number>(<EPtr>right).loadValue()) >>> 0);
            });

        this._map.set(
            "native float operator+<>(float,float)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box(Math.fround(<number>(<EPtr>left).loadValue() + <number>(<EPtr>right).loadValue()));
            });

        this._map.set(
            "native double operator+<>(double,double)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box(<number>(<EPtr>left).loadValue() + <number>(<EPtr>right).loadValue());
            });

        this._map.set(
            "native int operator-<>(int,int)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box(((<number>(<EPtr>left).loadValue() - <number>(<EPtr>right).loadValue()) | 0));
            });

        this._map.set(
            "native uint operator-<>(uint,uint)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<number>(<EPtr>left).loadValue() - <number>(<EPtr>right).loadValue()) >>> 0);
            });

        this._map.set(
            "native float operator-<>(float,float)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box(Math.fround(<number>(<EPtr>left).loadValue() - <number>((<EPtr>right).loadValue())));
            });

        this._map.set(
            "native double operator-<>(double,double)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box(<number>(<EPtr>left).loadValue() - <number>(<EPtr>right).loadValue());
            });

        this._map.set(
            "native int operator*<>(int,int)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<number>(<EPtr>left).loadValue() * <number>(<EPtr>right).loadValue()) | 0);
            });

        this._map.set(
            "native uint operator*<>(uint,uint)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<number>(<EPtr>left).loadValue() * <number>(<EPtr>right).loadValue()) >>> 0);
            });

        this._map.set(
            "native float operator*<>(float,float)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box(Math.fround(<number>(<EPtr>left).loadValue() * <number>(<EPtr>right).loadValue()));
            });

        this._map.set(
            "native double operator*<>(double,double)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<number>(<EPtr>left).loadValue()) * <number>(<EPtr>right).loadValue());
            });

        this._map.set(
            "native int operator/<>(int,int)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<number>(<EPtr>left).loadValue() / <number>(<EPtr>right).loadValue()) | 0);
            });

        this._map.set(
            "native uint operator/<>(uint,uint)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<number>(<EPtr>left).loadValue() / <number>(<EPtr>right).loadValue()) >>> 0);
            });

        this._map.set(
            "native int operator&<>(int,int)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box(<number>(<EPtr>left).loadValue() & <number>(<EPtr>right).loadValue());
            });

        this._map.set(
            "native uint operator&<>(uint,uint)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<number>(<EPtr>left).loadValue() & <number>(<EPtr>right).loadValue()) >>> 0);
            });

        this._map.set(
            "native int operator|<>(int,int)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box(<number>(<EPtr>left).loadValue() | <number>(<EPtr>right).loadValue());
            });

        this._map.set(
            "native uint operator|<>(uint,uint)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<number>(<EPtr>left).loadValue() | <number>(<EPtr>right).loadValue()) >>> 0);
            });

        this._map.set(
            "native int operator^<>(int,int)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box(<number>(<EPtr>left).loadValue() ^ <number>(<EPtr>right).loadValue());
            });

        this._map.set(
            "native uint operator^<>(uint,uint)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<number>(<EPtr>left).loadValue() ^ <number>(<EPtr>right).loadValue()) >>> 0);
            });

        this._map.set(
            "native int operator<<<>(int,uint)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box(<number>(<EPtr>left).loadValue() << <number>(<EPtr>right).loadValue());
            });

        this._map.set(
            "native uint operator<<<>(uint,uint)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<number>(<EPtr>left).loadValue() << <number>(<EPtr>right).loadValue()) >>> 0);
            });

        this._map.set(
            "native int operator>><>(int,uint)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box(<number>(<EPtr>left).loadValue() >> <number>(<EPtr>right).loadValue());
            });

        this._map.set(
            "native uint operator>><>(uint,uint)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box(<number>(<EPtr>left).loadValue() >>> <number>(<EPtr>right).loadValue());
            });

        this._map.set(
            "native int operator~<>(int)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box(~(<EPtr>value).loadValue());
            });

        this._map.set(
            "native uint operator~<>(uint)",
            (func: Func) => {
                func.implementation = ([value]) => EPtr.box((~(<EPtr>value).loadValue()) >>> 0);
            });

        this._map.set(
            "native float operator/<>(float,float)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box(Math.fround(<number>(<EPtr>left).loadValue() / <number>(<EPtr>right).loadValue()));
            });

        this._map.set(
            "native double operator/<>(double,double)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box(<number>(<EPtr>left).loadValue() / <number>(<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator==<>(int,int)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() == (<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator==<>(uint,uint)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() == (<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator==<>(bool,bool)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() == (<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator==<>(float,float)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() == (<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator==<>(double,double)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() == (<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator<<>(int,int)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() < (<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator<<>(uint,uint)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() < (<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator<<>(float,float)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() < (<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator<<>(double,double)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() < (<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator<=<>(int,int)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() <= (<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator<=<>(uint,uint)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() <= (<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator<=<>(float,float)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() <= (<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator<=<>(double,double)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() <= (<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator><>(int,int)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() > (<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator><>(uint,uint)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() > (<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator><>(float,float)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() > (<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator><>(double,double)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() > (<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator>=<>(int,int)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() >= (<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator>=<>(uint,uint)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() >= (<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator>=<>(float,float)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() >= (<EPtr>right).loadValue());
            });

        this._map.set(
            "native bool operator>=<>(double,double)",
            (func: Func) => {
                func.implementation = ([left, right]) =>
                    EPtr.box((<EPtr>left).loadValue() >= (<EPtr>right).loadValue());
            });

        for (let addressSpace of addressSpaces) {
            this._map.set(
                `native T* ${addressSpace} operator&[]<T>(T[] ${addressSpace},uint)`,
                (func: Func) => {
                    func.implementation = ([ref, index], node) => {
                        let ref_v = <EArrayRef>(<EPtr>ref).loadValue();
                        if (!ref_v)
                            throw new WTrapError(node._origin.originString, "Null dereference");
                        let index_v = <number>(<EPtr>index).loadValue();
                        if (index_v > ref_v.length)
                            throw new WTrapError(node._origin.originString, "Array index " + index + " is out of bounds of " + ref);
                        return EPtr.box(ref_v.ptr.plus(index_v * (<CallExpression>node).instantiatedActualTypeArguments[0].size));
                    };
                });

            this._map.set(
                `native uint operator.length<T>(T[] ${addressSpace})`,
                (func: Func) => {
                    func.implementation = ([ref], node) => {
                        let ref_v = <EArrayRef>(<EPtr>ref).loadValue();
                        if (!ref_v)
                            return EPtr.box(0);
                        return EPtr.box(ref_v.length);
                    };
                });
        }
    }

    add(thing: Func | Type)
    {
        let intrinsic = this._map.get((<Node1>thing).toString());
        if (!intrinsic)
            throw new WTypeError(thing._origin.originString, "Unrecognized intrinsic: " + thing);
        intrinsic(thing);
    }
}


class LateChecker extends Visitor {
    visitReferenceType(node: ReferenceType)
    {
        if (node.addressSpace == "thread")
            return;

        if (!node.elementType.isPrimitive)
            throw new WTypeError(node.origin.originString, "Illegal pointer to non-primitive type: " + node);
    }

    _checkShaderType(node: Func)
    {
        // FIXME: Tighten these checks. For now, we should only accept int32, uint32, float32, and float64.
        let assertPrimitive = (type: Type) => {
            if (!type.isPrimitive)
                throw new WTypeError(node.origin.originString, "Shader signature cannot include non-primitive type: " + type);
        }
        assertPrimitive(node.returnType);
        if (!(node.returnType.unifyNode instanceof StructType))
            throw new WTypeError(node.origin.originString, "Vertex shader " + node.name + " must return a struct.");
        switch (node.shaderType) {
        case "vertex":
            for (let parameter of node.parameters) {
                if (parameter.type.unifyNode instanceof StructType)
                    assertPrimitive(parameter.type);
                else if (!(<Type>parameter.type.unifyNode).isArrayRef)
                    throw new WTypeError(node.origin.originString, node.name + " accepts a parameter " + parameter.name + " which isn't a struct and isn't an ArrayRef.");
            }
            break;
        case "fragment":
            for (let parameter of node.parameters) {
                if (parameter.name == "stageIn") {
                    if (!(parameter.type.unifyNode instanceof StructType))
                        throw new WTypeError(node.origin.originString, "Fragment entry points' stageIn parameter (of " + node.name + ") must be a struct type.");
                    assertPrimitive(parameter.type);
                } else {
                    if (!(<Type>parameter.type.unifyNode).isArrayRef)
                        throw new WTypeError(node.origin.originString, "Fragment entry point's " + parameter.name + " parameter is not an array reference.");
                }
            }
            break;
        default:
            throw new Error("Bad shader type: " + node.shaderType);
        }
    }

    visitFuncDef(node: FuncDef)
    {
        if (node.shaderType)
            this._checkShaderType(node);
        super.visitFuncDef(node);
    }
}

class Lexer {
    _origin: string;
    _originKind: string;
    _lineNumberOffset: int;
    _text: string;
    _index: int;
    _stack: LexerToken[];
    constructor(origin: string, originKind: string, lineNumberOffset: int, text: string)
    {
        if (!isOriginKind(originKind))
            throw new Error("Bad origin kind: " + originKind);
        this._origin = origin;
        this._originKind = originKind;
        this._lineNumberOffset = lineNumberOffset;
        this._text = text;
        this._index = 0;
        this._stack = [];
    }

    get origin() {
        return this._origin;
    }
    get lineNumber(): int
    {
        return this.lineNumberForIndex(this._index);
    }

    get originString()
    {
        return this._origin + ":" + (this.lineNumber + 1);
    }

    get originKind() { return this._originKind; }

    lineNumberForIndex(index: int): int
    {
        let matches = <Array<string>>this._text.substring(0, index).match(/\n/g);
        return (matches ? <int>matches.length : 0) + this._lineNumberOffset;
    }

    get state() { return {index: this._index, stack: this._stack.concat()}; }
    set state(value)
    {
        this._index = value.index;
        this._stack = value.stack;
    }

    static _textIsIdentifierImpl(text: string)
    {
        return /^[^\d\W]\w*/.test(text);
    }

    static textIsIdentifier(text: string): boolean
    {
        return Lexer._textIsIdentifierImpl(text) && !(<RegExpType><unknown>RegExp).rightContext.length;
    }

    next()
    {
        if (this._stack.length)
            return this._stack.pop();

        if (this._index >= this._text.length)
            return null;

        const isCCommentBegin = /\/\*/;
        const isCPlusPlusCommentBegin = /\/\//;

        let result = (kind: string) => {
            let text = RegExp.lastMatch;
            let token = new LexerToken(this, this._index, kind, text);
            this._index += text.length;
            return token;
        };

        let relevantText: string;
        for (;;) {
            relevantText = this._text.substring(this._index);
            if (/^\s+/.test(relevantText)) {
                this._index += RegExp.lastMatch.length;
                continue;
            }
            if (/^\/\*/.test(relevantText)) {
                let endIndex = relevantText.search(/\*\//);
                if (endIndex < 0)
                    this.fail("Unterminated comment");
                this._index += endIndex;
                continue;
            }
            if (/^\/\/.*/.test(relevantText)) {
                this._index += RegExp.lastMatch.length;
                continue;
            }
            break;
        }

        if (this._index >= this._text.length)
            return null;

        // FIXME: Make this handle exp-form literals like 1e1.
        if (/^(([0-9]*\.[0-9]+[fd]?)|([0-9]+\.[0-9]*[fd]?))/.test(relevantText))
            return result("floatLiteral");

        // FIXME: Make this do Unicode.
        if (Lexer._textIsIdentifierImpl(relevantText)) {
            if (/^(struct|protocol|typedef|if|else|enum|continue|break|switch|case|default|for|while|do|return|constant|device|threadgroup|thread|operator|null|true|false)$/.test(RegExp.lastMatch))
                return result("keyword");

            if (this._originKind == "native" && /^(native|restricted)$/.test(RegExp.lastMatch))
                return result("keyword");

            return result("identifier");
        }

        if (/^0x[0-9a-fA-F]+u/.test(relevantText))
            return result("uintHexLiteral");

        if (/^0x[0-9a-fA-F]+/.test(relevantText))
            return result("intHexLiteral");

        if (/^[0-9]+u/.test(relevantText))
            return result("uintLiteral");

        if (/^[0-9]+/.test(relevantText))
            return result("intLiteral");

        if (/^<<|>>|->|>=|<=|==|!=|\+=|-=|\*=|\/=|%=|\^=|\|=|&=|\+\+|--|&&|\|\||([{}()\[\]?:=+*\/,.%!~^&|<>@;-])/.test(relevantText))
            return result("punctuation");

        let remaining = relevantText.substring(0, 20).split(/\s/)[0];
        if (!remaining.length)
            remaining = relevantText.substring(0, 20);
        this.fail("Unrecognized token beginning with: " + remaining);
    }

    push(token: LexerToken)
    {
        this._stack.push(token);
    }

    peek()
    {
        let result = this.next();
        this.push(result);
        return result;
    }

    fail(error: string)
    {
        throw new WSyntaxError(this.originString, error);
    }

    backtrackingScope(callback: ()=>Node1|void)
    {
        let state = this.state;
        try {
            return callback();
        } catch (e) {
            if (e instanceof WSyntaxError) {
                this.state = state;
                return null;
            }
            throw e;
        }
    }

    testScope(callback: ()=>void)
    {
        let state = this.state;
        try {
            callback();
            return true;
        } catch (e) {
            if (e instanceof WSyntaxError)
                return false;
            throw e;
        } finally {
            this.state = state;
        }
    }
}

class LexerToken {
    _lexer: Lexer;
    _index: int;
    _kind: string;
    _text: string;
    constructor(lexer: Lexer, index: int, kind: string, text: string)
    {
        this._lexer = lexer;
        this._index = index;
        this._kind = kind;
        this._text = text;
    }

    get lexer()
    {
        return this._lexer;
    }

    get kind()
    {
        return this._kind;
    }

    get text()
    {
        return this._text;
    }

    get origin()
    {
        return this.lexer.origin;
    }

    get originKind()
    {
        return this.lexer.originKind;
    }

    get index()
    {
        return this._index;
    }

    get lineNumber()
    {
        return this._lexer.lineNumberForIndex(this._index);
    }

    get originString()
    {
        return this.origin + ":" + (this.lineNumber + 1);
    }

    toString()
    {
        return "LexerToken(" + this.kind + ", " + this.text + ", " + this.lineNumber + ")";
    }
}

class LiteralTypeChecker extends Visitor {
    visitNullType(node: NullType)
    {
        if (!node.type)
            throw new Error("Null at " + node.origin.originString + " does not have type");
    }

    visitGenericLiteralType(node: GenericLiteralType)
    {
        if (!node.type)
            throw new Error(node + " at " + node.origin.originString + " does not have type");
    }
}

class LogicalExpression extends Expression {
    _text: string;
    _left: CallExpression | FunctionLikeBlock;
    _right: CallExpression | FunctionLikeBlock;
    constructor(origin: Origin, text: string, left: CallExpression, right: CallExpression)
    {
        super(origin);
        this._text = text;
        this._left = left;
        this._right = right;
    }

    get text() { return this._text; }
    get left() { return this._left; }
    get right() { return this._right; }

    toString()
    {
        return "(" + this.left + " " + this.text + " " + this.right + ")";
    }
}

class LogicalNot extends Expression {
    _operand: CallExpression | FunctionLikeBlock;
    constructor(origin: Origin, operand: CallExpression)
    {
        super(origin);
        this._operand = operand;
    }

    get operand() { return this._operand; }

    toString()
    {
        return "!(" + this.operand + ")";
    }
}

class LoopChecker extends Visitor {
    _loopDepth: number;
    _switchDepth: number;
    constructor()
    {
        super();
        this._loopDepth = 0;
        this._switchDepth = 0;
    }

    visitFuncDef(node: FuncDef)
    {
        if (this._loopDepth != 0)
            throw new WTypeError(node.origin.originString, "LoopChecker does not understand nested functions.");
        super.visitFuncDef(node);
    }

    visitWhileLoop(node: WhileLoop)
    {
        node.conditional.visit(this);
        ++this._loopDepth;
        node.body.visit(this);
        if (this._loopDepth == 0)
            throw new WTypeError(node.origin.originString, "The number of nested loops is negative!");
        --this._loopDepth;
    }

    visitDoWhileLoop(node: DoWhileLoop)
    {
        ++this._loopDepth;
        node.body.visit(this);
        if (this._loopDepth == 0)
            throw new WTypeError(node.origin.originString, "The number of nested loops is negative!");
        --this._loopDepth;
        node.conditional.visit(this);
    }

    visitForLoop(node: ForLoop)
    {
        if (node.initialization)
            node.initialization.visit(this);
        if (node.condition)
            node.condition.visit(this);
        if (node.increment)
            node.increment.visit(this);
        ++this._loopDepth;
        node.body.visit(this);
        if (this._loopDepth == 0)
            throw new WTypeError(node.origin.originString, "The number of nested loops is negative!");
        --this._loopDepth;
    }

    visitSwitchStatement(node: SwitchStatement)
    {
        node.value.visit(this);
        this._switchDepth++;
        for (let switchCase of node.switchCases)
            switchCase.visit(this);
        this._switchDepth--;
    }

    visitBreak(node: Break)
    {
        if (this._loopDepth == 0 && this._switchDepth == 0)
            throw new WTypeError(node.origin.originString, "Break statement without enclosing loop or switch!");
        super.visitBreak(node);
    }

    visitContinue(node: Continue)
    {
        if (this._loopDepth == 0)
            throw new WTypeError(node.origin.originString, "Continue statement without enclosing loop!");
        super.visitContinue(node);
    }
}

class MakeArrayRefExpression extends Expression {
    numElements: Expression;
    _lValue: Value;
    constructor(origin: Origin, lValue: Value)
    {
        super(origin);
        this._lValue = lValue;
    }

    get lValue() { return this._lValue; }

    toString()
    {
        return "@" + (this.numElements ? "<<" + this.numElements + ">>" : "") + "(" + this.lValue + ")";
    }
}

class MakePtrExpression extends Expression {
    _lValue: Node1;
    constructor(origin: Origin, lValue: Node1)
    {
        super(origin);
        this._lValue = lValue;
    }

    get lValue() { return this._lValue; }

    toString()
    {
        return "&(" + this.lValue + ")";
    }
}

const Anything = Symbol();

function isWildcardKind(kind: object)
{
    return kind == <object><unknown>Anything;
}

type FuncArray = {kind: object, array: Func[]};

class NameContext {
    _map: Map<string, Node1|FuncArray>
    _set: Set<Node1>;
    _currentStatement: Node1;
    _delegate: NameContext;
    _intrinsics: Intrinsics;
    _program: Program;
    constructor(delegate?: NameContext)
    {
        this._map = new Map();
        this._set = new Set();
        this._currentStatement = null;
        this._delegate = delegate;
        this._intrinsics = null;
        this._program = null;
    }

    add(thing: Node1)
    {
        if (!thing.name)
            return;
        if (!thing.origin)
            throw new Error("Thing does not have origin: " + thing);

        if (thing.isNative && !thing.implementation) {
            if (!this._intrinsics)
                throw new Error("Native function in a scope that does not recognize intrinsics");
            this._intrinsics.add(<Func>thing);
        }

        if (thing.kind == Func) {
            this._set.add(thing);
            let array = this._map.get(thing.name);
            if (!array) {
                let arrayData: Func[] = [];
                array = {kind: Func, array: arrayData};
                this._map.set(thing.name, array);
            }
            if (array.kind != Func)
                throw new WTypeError(thing.origin.originString, "Cannot reuse type name for function: " + thing.name);
            (<FuncArray>array).array.push(<Func>thing);
            return;
        }
        if (this._map.has(thing.name))
            throw new WTypeError(thing.origin.originString, "Duplicate name: " + thing.name);
        this._set.add(thing);
        this._map.set(thing.name, thing);
    }

    get(kind: object, name: string): Node1 | FuncArray
    {
        let result = this._map.get(name);
        if (!result && this._delegate)
            return this._delegate.get(kind, name);
        if (result && !isWildcardKind(kind) && result.kind != kind)
            return null;

        return result;
    }

    underlyingThings(kind: object, name: string)
    {
        let things = this.get(kind, name);
        return NameContext.underlyingThings(things);
    }

    static *underlyingThings(thing: Node1 | FuncArray)
    {
        if (!thing)
            return;
        if (thing.kind === Func) {
            let funcArray: Func[] = (<FuncArray>thing).array;
            if (!(funcArray instanceof Array))
                throw new Error("Func thing is not array: " + funcArray);
            for (let func of funcArray)
                yield func;
            return;
        }
        yield <Node1>thing;
    }

    resolveFuncOverload(name: string, typeArguments: Type[], argumentTypes: Type[], returnType: Type, allowEntryPoint = false): OverLoad
    {
        let functions = this.get(Func, name);
        if (!functions)
            return {failures: [], func: undefined};

        return resolveOverloadImpl((<FuncArray>functions).array, typeArguments, argumentTypes, returnType, allowEntryPoint);
    }

    get currentStatement(): Node1
    {
        if (this._currentStatement)
            return this._currentStatement;
        if (this._delegate)
            return this._delegate.currentStatement;
        return null;
    }

    doStatement(statement: Node1, callback: ()=>void)
    {
        this._currentStatement = statement;
        callback();
        this._currentStatement = null;
    }

    recognizeIntrinsics()
    {
        this._intrinsics = new Intrinsics(this);
    }

    get intrinsics(): Intrinsics
    {
        if (this._intrinsics)
            return this._intrinsics;
        if (this._delegate)
            return this._delegate.intrinsics;
        return null;
    }

    set program(value: Program)
    {
        this._program = value;
    }

    get program()
    {
        if (this._program)
            return this._program;
        if (this._delegate)
            return this._delegate.program;
        return null;
    }

    *[Symbol.iterator]()
    {
        for (let value of this._map.values()) {
            if (value instanceof Array) {
                for (let func of value)
                    yield func;
                continue;
            }
            yield value;
        }
    }
}

class NameFinder extends Visitor {
    _set: Set<string>
    _worklist: string[];
    constructor()
    {
        super();
        this._set = new Set();
        this._worklist = [];
    }

    get set() { return this._set; }
    get worklist() { return this._worklist; }

    add(name: string)
    {
        if (this._set.has(name))
            return;
        this._set.add(name);
        this._worklist.push(name);
    }

    visitProtocolRef(node: ProtocolRef)
    {
        this.add(node.name);
        super.visitProtocolRef(node);
    }

    visitTypeRef(node: TypeRef)
    {
        this.add(node.name);
        super.visitTypeRef(node);
    }

    visitVariableRef(node: VariableRef)
    {
        this.add(node.name);
        super.visitVariableRef(node);
    }

    visitTypeOrVariableRef(node: TypeOrVariableRef)
    {
        this.add(node.name);
    }

    _handlePropertyAccess(node: PropertyAccessExpression)
    {
        this.add(node.getFuncName);
        this.add(node.setFuncName);
        this.add(node.andFuncName);
    }

    visitDotExpression(node: DotExpression)
    {
        this._handlePropertyAccess(node);
        super.visitDotExpression(node);
    }

    visitIndexExpression(node: IndexExpression)
    {
        this._handlePropertyAccess(node);
        super.visitIndexExpression(node);
    }

    visitCallExpression(node: CallExpression)
    {
        this.add(node.name);
        super.visitCallExpression(node);
    }
}

// checked before TypeRefToTypeDefSkipper.
class NameResolver extends Visitor {
    _nameContext: NameContext;
    constructor(nameContext: NameContext)
    {
        super();
        this._nameContext = nameContext;
    }

    doStatement(statement: Node1)
    {
        this._nameContext.doStatement(statement, () => statement.visit(this));
    }

    _visitTypeParametersAndBuildNameContext(node: Func)
    {
        let nameContext = new NameContext(this._nameContext);
        for (let typeParameter of node.typeParameters) {
            nameContext.add(typeParameter);
            typeParameter.visit(this);
        }
        return nameContext;
    }

    visitFunc(node: Func)
    {
        let checker = new NameResolver(this._visitTypeParametersAndBuildNameContext(node));
        node.returnType.visit(checker);
        for (let parameter of node.parameters)
            parameter.visit(checker);
    }

    visitFuncDef(node: FuncDef)
    {
        let contextWithTypeParameters = this._visitTypeParametersAndBuildNameContext(node);
        let checkerWithTypeParameters = new NameResolver(contextWithTypeParameters);
        node.returnType.visit(checkerWithTypeParameters);
        let contextWithParameters = new NameContext(contextWithTypeParameters);
        for (let parameter of node.parameters) {
            parameter.visit(checkerWithTypeParameters);
            contextWithParameters.add(parameter);
        }
        let checkerWithParameters = new NameResolver(contextWithParameters);
        node.body.visit(checkerWithParameters);
    }

    visitBlock(node: Block)
    {
        let checker = new NameResolver(new NameContext(this._nameContext));
        for (let statement of node.statements)
            statement.visit(checker);
    }

    visitIfStatement(node: IfStatement)
    {
        node.conditional.visit(this);
        // The bodies might not be Blocks, so we need to explicitly give them a new context.
        node.body.visit(new NameResolver(new NameContext(this._nameContext)));
        if (node.elseBody)
            node.elseBody.visit(new NameResolver(new NameContext(this._nameContext)));
    }

    visitWhileLoop(node: WhileLoop)
    {
        node.conditional.visit(this);
        // The bodies might not be Blocks, so we need to explicitly give them a new context.
        node.body.visit(new NameResolver(new NameContext(this._nameContext)));
    }

    visitDoWhileLoop(node: DoWhileLoop)
    {
        // The bodies might not be Blocks, so we need to explicitly give them a new context.
        node.body.visit(new NameResolver(new NameContext(this._nameContext)));
        node.conditional.visit(this);
    }

    visitForLoop(node: ForLoop)
    {
        let newResolver = new NameResolver(new NameContext(this._nameContext))
        if (node.initialization)
            node.initialization.visit(newResolver);
        if (node.condition)
            node.condition.visit(newResolver);
        if (node.increment)
            node.increment.visit(newResolver);
        node.body.visit(newResolver);
    }

    visitProtocolDecl(node: ProtocolDecl)
    {
        for (let parent of node.extends)
            parent.visit(this);
        let nameContext = new NameContext(this._nameContext);
        nameContext.add(node.typeVariable);
        let checker = new NameResolver(nameContext);
        for (let signature of node.signatures)
            signature.visit(checker);
    }

    visitProtocolRef(node: ProtocolRef)
    {
        let result = this._nameContext.get(Protocol, node.name);
        if (!result)
            throw new WTypeError(node.origin.originString, "Could not find protocol named " + node.name);
        node.protocolDecl = <ProtocolDecl>result;
    }

    visitProtocolFuncDecl(node: ProtocolFuncDecl)
    {
        this.visitFunc(node);
        let funcs = this._nameContext.get(Func, node.name);
        if (!funcs)
            throw new WTypeError(node.origin.originString, "Cannot find any functions named " + node.name);
        node.possibleOverloads = (<FuncArray>funcs).array;
    }

    visitTypeDef(node: TypeDef)
    {
        let nameContext = new NameContext(this._nameContext);
        for (let typeParameter of node.typeParameters) {
            typeParameter.visit(this);
            nameContext.add(typeParameter);
        }
        let checker = new NameResolver(nameContext);
        node.type.visit(checker);
    }

    visitStructType(node: StructType): undefined
    {
        let nameContext = new NameContext(this._nameContext);
        for (let typeParameter of node.typeParameters) {
            typeParameter.visit(this);
            nameContext.add(typeParameter);
        }
        let checker = new NameResolver(nameContext);
        for (let field of node.fields)
            field.visit(checker);

        return undefined;
    }

    _resolveTypeArguments(typeArguments: Node1[])
    {
        for (let i = 0; i < typeArguments.length; ++i) {
            let typeArgument = typeArguments[i];
            if (typeArgument instanceof TypeOrVariableRef) {
                let thing = this._nameContext.get(<object><unknown>Anything, typeArgument.name);
                if (!thing)
                    new WTypeError(typeArgument.origin.originString, "Could not find type or variable named " + typeArgument.name);
                if (thing instanceof Value)
                    typeArguments[i] = new VariableRef(typeArgument.origin, typeArgument.name);
                else if (thing instanceof Type)
                    typeArguments[i] = new TypeRef(typeArgument.origin, typeArgument.name, []);
                else
                    throw new WTypeError(typeArgument.origin.originString, "Type argument resolved to wrong kind of thing: " + thing.kind);
            }

            if ((<Node1[]><unknown>typeArgument)[i] instanceof Value
                && !(<Value[]><unknown>typeArgument)[i].isConstexpr)
                throw new WTypeError((<Value[]><unknown>typeArgument)[i].origin.originString, "Expected constexpr");
        }
    }

    visitTypeRef(node: TypeRef)
    {
        this._resolveTypeArguments(node.typeArguments);

        let type = node.type;
        if (!type) {
            type = <Type>this._nameContext.get(Type, node.name);
            if (!type)
                throw new WTypeError(node.origin.originString, "Could not find type named " + node.name);
            node.type = type;
        }

        if (type.typeParameters.length != node.typeArguments.length)
            throw new WTypeError(node.origin.originString, "Wrong number of type arguments (passed " + node.typeArguments.length + ", expected " + type.typeParameters.length + ")");
        for (let i = 0; i < type.typeParameters.length; ++i) {
            let parameterIsType = type.typeParameters[i] instanceof TypeVariable;
            let argumentIsType = node.typeArguments[i] instanceof Type;
            node.typeArguments[i].visit(this);
            if (parameterIsType && !argumentIsType)
                throw new WTypeError(node.origin.originString, "Expected type, but got value at argument #" + i);
            if (!parameterIsType && argumentIsType)
                throw new WTypeError(node.origin.originString, "Expected value, but got type at argument #" + i);
        }
    }

    visitReferenceType(node: ReferenceType)
    {
        let nameContext = new NameContext(this._nameContext);
        node.elementType.visit(new NameResolver(nameContext));
    }

    visitVariableDecl(node: VariableDecl)
    {
        this._nameContext.add(node);
        node.type.visit(this);
        if (node.initializer)
            node.initializer.visit(this);
    }

    visitVariableRef(node: VariableRef)
    {
        if (node.variable)
            return;
        let result = this._nameContext.get(Value, node.name);
        if (!result)
            throw new WTypeError(node.origin.originString, "Could not find variable named " + node.name);
        node.variable = <Value>result;
    }

    visitReturn(node: Return)
    {
        node.func = <Func>this._nameContext.currentStatement;
        super.visitReturn(node);
    }

    _handlePropertyAccess(node: PropertyAccessExpression)
    {
        let value: FuncArray = <FuncArray>this._nameContext.get(Func, node.getFuncName);
        node.possibleGetOverloads = value? value.array : undefined;
        value = <FuncArray>this._nameContext.get(Func, node.setFuncName);
        node.possibleSetOverloads = value? value.array : undefined;
        value = <FuncArray>this._nameContext.get(Func, node.andFuncName)
        node.possibleAndOverloads = value? value.array : undefined;

        if (!node.possibleGetOverloads && !node.possibleAndOverloads)
            throw new WTypeError(node.origin.originString, "Cannot find either " + node.getFuncName + " or " + node.andFuncName);
    }

    visitDotExpression(node: DotExpression)
    {
        // This could be a reference to an enum. Let's resolve that now.
        if (node.struct1 instanceof VariableRef) {
            let enumType = this._nameContext.get(Type, node.struct1.name);
            if (enumType && enumType instanceof EnumType) {
                let enumMember = enumType.memberByName(node.fieldName);
                if (!enumMember)
                    throw new WTypeError(node.origin.originString, "Enum " + enumType.name + " does not have a member named " + node.fieldName);
                node.become(new EnumLiteral(node.origin, enumMember));
                return;
            }
        }

        this._handlePropertyAccess(node);
        super.visitDotExpression(node);
    }

    visitIndexExpression(node: IndexExpression)
    {
        this._handlePropertyAccess(node);
        super.visitIndexExpression(node);
    }

    visitCallExpression(node: CallExpression)
    {
        this._resolveTypeArguments(node.typeArguments);

        let funcs = <FuncArray>this._nameContext.get(Func, node.name);
        if (funcs)
            node.possibleOverloads = funcs.array;
        else {
            let type = <Type>this._nameContext.get(Type, node.name);
            if (!type)
                throw new WTypeError(node.origin.originString, "Cannot find any function or type named \"" + node.name + "\"");
            node.becomeCast(type);
            let ret :FuncArray = <FuncArray>this._nameContext.get(Func, "operator cast");
            node.possibleOverloads = ret.array;
            if (!node.possibleOverloads)
                throw new WTypeError(node.origin.originString, "Cannot find any operator cast implementations in cast to " + type);
        }

        super.visitCallExpression(node);
    }
}

class NativeFunc extends Func {
    instantiateImplementation: (substitution: InstantiationSubstitution)=>ImplementationDataType;
    visitImplementationData: (implementationData: ImplementationDataType, visitor: Visitor) => void;
    didLayoutStructsInImplementationData: (implementationData: ImplementationDataType)=>void;
    isRestricted: boolean;
    constructor(origin: Origin, name: string, returnType: Type, typeParameters: Node1[], parameters: FuncParameter[], isCast: boolean, shaderType: string)
    {
        super(origin, name, returnType, typeParameters, parameters, isCast, shaderType);
        this.isRestricted = false;
        this.implementation = null;
        this.instantiateImplementation = (substitution) => DefaultImplementationData;
        this.visitImplementationData = (implementationData: ImplementationDataType, visitor: Visitor) => null;
        this.didLayoutStructsInImplementationData = implementationData => null;
    }

    get isNative() { return true; }

    toDeclString()
    {
        return "native " + super.toDeclString();
    }
}

class NativeFuncInstance extends Func {
    _func: NativeFunc;
    _implementationData: ImplementationDataType;
    constructor(func: NativeFunc, returnType: Type, parameters: FuncParameter[], isCast: boolean, shaderType: string, implementationData: ImplementationDataType)
    {
        super(func.origin, func.name, returnType, [], parameters, isCast, shaderType);
        this._func = func;
        this._implementationData = implementationData;
    }

    get func() { return this._func; }
    get isNative() { return true; }
    get implementationData() { return this._implementationData; }

    toDeclString()
    {
        return "native " + super.toDeclString();
    }
}

function NativeTypeInstantiate(typeArguments: Type[]) {
    if (typeArguments.length != this.typeParameters.length)
        throw new Error("Wrong number of type arguments to instantiation");
    if (!typeArguments.length)
        return this;
    return new NativeTypeInstance(this, typeArguments);
}

class NativeType extends Type {
    _origin: Origin;
    _name: string;
    _typeParameters: TypeParameters[];
    _isNumber: boolean;
    _isInt: boolean;
    _isFloating: boolean;
    _isPrimitive: boolean;
    constructor(origin: Origin, name: string, typeParameters: TypeParameters[])
    {
        if (!(typeParameters instanceof Array))
            throw new Error("type parameters not array: " + typeParameters);
        super();
        this._origin = origin;
        this._name = name;
        this._typeParameters = typeParameters;
        this._isNumber = false;
        this._isInt = false;
        this._isFloating = false;
        this._isPrimitive = false;
        this.instantiate = NativeTypeInstantiate;
    }

    get typeParameters() { return this._typeParameters; }
    get isNative() { return true; }

    // We let Intrinsics.js set these as it likes.
    get isNumber() { return this._isNumber; }
    set isNumber(value) { this._isNumber = value; }
    get isInt() { return this._isInt; }
    set isInt(value) { this._isInt = value; }
    get isFloating() { return this._isFloating; }
    set isFloating(value) { this._isFloating = value; }
    get isPrimitive() { return this._isPrimitive; }
    set isPrimitive(value) { this._isPrimitive = value; }

    toString()
    {
        return "native typedef " + this.name + "<" + this.typeParameters + ">";
    }
}

class NativeTypeInstance extends Type {
    _typeArguments: Type[];
    constructor(type: Type, typeArguments: Type[])
    {
        super();
        this._type = type;
        this._typeArguments = typeArguments;
    }

    get typeArguments() { return this._typeArguments; }
    get isPrimitive() { return this._type.isPrimitive; }
    get isNative() { return true; }

    unifyImpl(unificationContext: UnificationContext, other: NativeTypeInstance)
    {
        if (this.type != other.type)
            return false;
        if (this.typeArguments.length != other.typeArguments.length)
            return false;
        for (let i = 0; i < this.typeArguments.length; ++i) {
            if (!this.typeArguments[i].unify(unificationContext, other.typeArguments[i]))
                return false;
        }
        return true;
    }

    toString()
    {
        return this.type + "<" + this.typeArguments + ">";
    }
}

class NormalUsePropertyResolver extends Rewriter {
    visitDotExpression(node: DotExpression): Node1
    {
        return (<DotExpression>super.visitDotExpression(node)).rewriteAfterCloning();
    }

    visitIndexExpression(node: IndexExpression): Node1
    {
        return (<IndexExpression>super.visitIndexExpression(node)).rewriteAfterCloning();
    }
}


class NullLiteral extends Expression {
    constructor(origin: Origin)
    {
        super(origin);
        this.type = new NullType(origin);
    }

    toString()
    {
        return "null";
    }
}

class NullType extends Type {
    constructor(origin: Origin)
    {
        super();
        this._origin = origin;
        this._isLiteral = true;
    }

    // For now we answer false for isPtr and isArrayRef because that happens to make all of the logic
    // work. But, it's strange, since as a bottom type it really is the case that this is both isPtr and
    // isArrayRef.

    get isPrimitive() { return true; }
    get isUnifiable() { return true; }

    typeVariableUnify(unificationContext: UnificationContext, other: Type)
    {
        if (!(other instanceof Type))
            return false;

        return this._typeVariableUnifyImpl(unificationContext, other);
    }

    unifyImpl(unificationContext: UnificationContext, other: Type)
    {
        return this.typeVariableUnify(unificationContext, other);
    }

    verifyAsArgument(unificationContext: UnificationContext): VerityResultType
    {
        let realThis = unificationContext.find(this);
        if (realThis.isPtr || (<Type>realThis).isArrayRef)
            return {result: true, reason: undefined};
        return {result: false, reason: "Null cannot be used with non-pointer type " + realThis};
    }

    verifyAsParameter(unificationContext: UnificationContext): VerityResultType
    {
        throw new Error("NullType should never be used as a type parameter");
    }

    commitUnification(unificationContext: UnificationContext)
    {
        this.type = <Type>unificationContext.find(this);
    }

    toString()
    {
        return "nullType";
    }
}

const originKinds = ["native", "user"];

function isOriginKind(originKind: string)
{
    switch (originKind) {
    case "native":
    case "user":
        return true;
    default:
        return false;
    }
}

class OverloadResolutionFailure {
    _func: Func;
    _reason: string;
    constructor(func: Func, reason: string)
    {
        this._func = func;
        this._reason = reason;
    }

    get func() { return this._func; }
    get reason() { return this._reason; }

    toString()
    {
        return this.func.toDeclString() + " did not match because: " + this.reason;
    }
}

class Parse {
    program: Program;
    origin: string;
    originKind: string;
    lineNumberOffset: int;
    text: string;
    lexer: Lexer;
    addressSpaceConsumed: boolean;
    static instance: Parse;
    constructor(program: Program, origin: string, originKind: string, lineNumberOffset: int, text: string) {
        this.program = program;
        this.origin = origin;
        this.originKind = originKind;
        this.lineNumberOffset = lineNumberOffset;
        this.text = text;
        this.lexer = new Lexer(origin, originKind, lineNumberOffset, text);
        Parse.instance = this;
    }

    static genericConsume(callback: (token: LexerToken)=>boolean, explanation: string[])
    {
        let token = Parse.instance.lexer.next();
        if (!token)
            Parse.instance.lexer.fail("Unexpected end of file");
        if (!callback(token))
            Parse.instance.lexer.fail("Unexpected token: " + token.text + "; expected: " + explanation);
        return token;
    }

    static consume(...texts: string[])
    {
        return Parse.genericConsume(token =>(texts.indexOf(token.text) != -1), texts);
    }

    static consumeKind(kind: string)
    {
        return Parse.genericConsume(token => token.kind == kind, [kind]);
    }

    static assertNext(...texts: string[])
    {
        Parse.instance.lexer.push(Parse.consume(...texts));
    }

    static genericTest(callback: (token: LexerToken)=>boolean)
    {
        let token = Parse.instance.lexer.peek();
        if (token && callback(token))
            return token;
        return null;
    }

    static test(...texts: string[])
    {
        return Parse.genericTest((token: LexerToken) => (texts.indexOf(token.text) != -1));
    }

    static testKind(kind: string)
    {
        return Parse.genericTest(token => token.kind == kind);
    }

    static tryConsume(...texts: string[])
    {
        let result = Parse.test(...texts);
        if (result)
            Parse.instance.lexer.next();
        return result;
    }

    static tryConsumeKind(kind: string)
    {
        let result = Parse.testKind(kind);
        if (result)
            Parse.instance.lexer.next();
        return result;
    }

    static parseProtocolRef()
    {
        let protocolToken = Parse.consumeKind("identifier");
        return new ProtocolRef(protocolToken, protocolToken.text);
    }

   static consumeEndOfTypeArgs()
    {
        let rightShift = Parse.tryConsume(">>");
        if (rightShift)
            Parse.instance.lexer.push(new LexerToken(Parse.instance.lexer, rightShift.index, rightShift.kind, ">"))
            // lexer.push(new LexerToken(lexer, rightShift, rightShift.index, rightShift.kind, ">")); XTS
        else
            this.consume(">");
    }

    static parseTypeParameters()
    {
        let result: (ConstexprTypeParameter|TypeVariable)[] = new Array();
        if (!Parse.test("<"))
            return result;

        Parse.consume("<");
        let assertNextFunc: Function = Parse.assertNext;
        while (!Parse.test(">")) {
            let constexpr: ConstexprTypeParameter = <ConstexprTypeParameter>Parse.instance.lexer.backtrackingScope(() => {
                let type = Parse.parseType();
                let name = Parse.consumeKind("identifier");
                assertNextFunc(",", ">", ">>");
                return new ConstexprTypeParameter(type.origin, name.text, type);
            });
            if (constexpr)
                result.push(constexpr);
            else {
                let name = Parse.consumeKind("identifier");
                let protocol = Parse.tryConsume(":") ? Parse.parseProtocolRef() : null;
                result.push(new TypeVariable(name, name.text, protocol));
            }
            if (!Parse.tryConsume(","))
                break;
        }
        Parse.consumeEndOfTypeArgs();
        return result;
    }

    static parseTerm()
    {
        let token: LexerToken;
        if (token = Parse.tryConsume("null"))
            return new NullLiteral(token);
        if (token = Parse.tryConsumeKind("identifier"))
            return new VariableRef(token, token.text);
        if (token = Parse.tryConsumeKind("intLiteral")) {
            let intVersion = (+token.text) | 0;
            if ("" + intVersion !== token.text)
                Parse.instance.lexer.fail("Integer literal is not an integer: " + token.text);
            return new IntLiteral(token, intVersion);
        }
        if (token = Parse.tryConsumeKind("uintLiteral")) {
            let uintVersion = (<number><unknown>token.text.substr(0, token.text.length - 1)) >>> 0;
            if (uintVersion + "u" !== token.text)
                Parse.instance.lexer.fail("Integer literal is not 32-bit unsigned integer: " + token.text);
            return new UintLiteral(token, uintVersion);
        }
        if ((token = Parse.tryConsumeKind("intHexLiteral"))
            || (token = Parse.tryConsumeKind("uintHexLiteral"))) {
            let hexString = token.text.substr(2);
            if (token.kind == "uintHexLiteral")
                hexString = hexString.substr(0, hexString.length - 1);
            if (!hexString.length)
                throw new Error("Bad hex literal: " + token);
            let intVersion = parseInt(hexString, 16);
            if (token.kind == "intHexLiteral")
                intVersion = intVersion | 0;
            else
                intVersion = intVersion >>> 0;
            /*
            if (intVersion.toString(16) !== hexString)
                Parse.instance.lexer.fail("Hex integer literal is not an integer: " + token.text);
            */
            if (token.kind == "intHexLiteral")
                return new IntLiteral(token, intVersion);
            return new UintLiteral(token, intVersion >>> 0);
        }
        if (token = Parse.tryConsumeKind("doubleLiteral"))
            return new DoubleLiteral(token, +token.text);
        if (token = Parse.tryConsumeKind("floatLiteral")) {
            let text = token.text;
            let d = token.text.endsWith("d");
            let f = token.text.endsWith("f");
            if (d && f)
                throw new Error("Literal cannot be both a double literal and a float literal.");
            if (d || f)
                text = text.substring(0, text.length - 1);
            let value = parseFloat(text);
            if (d)
                return new DoubleLiteral(token, value);
            return new FloatLiteral(token, Math.fround(value));
        }
        if (token = Parse.tryConsume("true", "false"))
            return new BoolLiteral(token, token.text == "true");
        // FIXME: Need support for other literals too.
        Parse.consume("(");
        let result = Parse.parseExpression();
        Parse.consume(")");
        return result;
    }

    static parseConstexpr()
    {
        let token: LexerToken;
        if (token = Parse.tryConsume("-"))
            return new CallExpression(token, "operator" + token.text, [], [Parse.parseTerm()]);
        let left = Parse.parseTerm();
        if (token = Parse.tryConsume("."))
            left = new DotExpression(token, left, Parse.consumeKind("identifier").text);
        return left;
    }

    static parseTypeArguments()
    {
        if (!Parse.test("<"))
            return [];

        let result = [];
        Parse.consume("<");
        let assertNextFunc: Function = Parse.assertNext;
        while (!Parse.test(">")) {
            // It's possible for a constexpr or type can syntactically overlap in the single
            // identifier case. Let's consider the possibilities:
            //
            //     T          could be type or constexpr
            //     T[]        only type
            //     T[42]      only type (constexpr cannot do indexing)
            //     42         only constexpr
            //
            // In the future we'll allow constexprs to do more things, and then we'll still have
            // the problem that something of the form T[1][2][3]... can either be a type or a
            // constexpr, and we can figure out in the checker which it is.
            let typeOrVariableRef = Parse.instance.lexer.backtrackingScope(() => {
                let result = Parse.consumeKind("identifier");
                assertNextFunc(",", ">", ">>");
                return new TypeOrVariableRef(result, result.text);
            });
            if (typeOrVariableRef)
                result.push(typeOrVariableRef);
            else {
                let constexpr = Parse.instance.lexer.backtrackingScope(() => {
                    let parseConstexprFunc: ()=>Expression = Parse.parseConstexpr;
                    let result = parseConstexprFunc();
                    assertNextFunc(",", ">", ">>");
                    return result;
                });
                if (constexpr)
                    result.push(constexpr);
                else
                    result.push(Parse.parseType());
            }
            if (!Parse.tryConsume(","))
                break;
        }
        Parse.consumeEndOfTypeArgs();
        return result;
    }

    static getAddressSpace(addressSpace: string)
    {
        Parse.instance.addressSpaceConsumed = true;
        if (addressSpace)
            return addressSpace;
        return Parse.consume(...addressSpaces).text;
    }

    static parseType()
    {
        let token: LexerToken;
        let addressSpace: string;
        Parse.instance.addressSpaceConsumed = false;
        if (token = Parse.tryConsume(...addressSpaces))
            addressSpace = token.text;

        let name = Parse.consumeKind("identifier");
        let typeArguments = Parse.parseTypeArguments();
        let type: Type = new TypeRef(name, name.text, typeArguments);



        while (token = Parse.tryConsume("*", "[")) {
            if (token.text == "*") {
                type = new PtrType(token, Parse.getAddressSpace(addressSpace), type);
                continue;
            }

            if (Parse.tryConsume("]")) {
                type = new ArrayRefType(token, Parse.getAddressSpace(addressSpace), type);
                continue;
            }

            type = new ArrayType(token, type, Parse.parseConstexpr());
            Parse.consume("]");
        }

        if (addressSpace && !Parse.instance.addressSpaceConsumed)
            Parse.instance.lexer.fail("Address space specified for type that does not need address space");

        return type;
    }

    static parseTypeDef()
    {
        let origin = Parse.consume("typedef");
        let name = Parse.consumeKind("identifier").text;
        let typeParameters = Parse.parseTypeParameters();
        Parse.consume("=");
        let type = Parse.parseType();
        Parse.consume(";");
        return new TypeDef(origin, name, typeParameters, type);
    }

    static genericParseLeft(texts: string[], nextParser: ()=>Expression, constructor: (token: LexerToken, left: Expression, right: Expression)=>Expression)
    {
        let left = nextParser();
        let token: LexerToken;
        while (token = Parse.tryConsume(...texts))
            left = <Expression>constructor(token, left, nextParser());
        return left;
    }

    static parseLeftOperatorCall(texts: string[], nextParser: ()=>Expression)
    {
        //TODO: anomoy function has no feedback
        let emptyArray: Node1[] = [];
        return Parse.genericParseLeft(
            texts, nextParser,
            (token, left, right) =>
                new CallExpression(token, "operator" + token.text, emptyArray, [left, right]));
    }

    static parseCallExpression()
    {
        let name = Parse.consumeKind("identifier");
        let typeArguments = Parse.parseTypeArguments();
        Parse.consume("(");
        let argumentList = [];
        while (!Parse.test(")")) {
            let argument = Parse.parsePossibleAssignment();
            argumentList.push(argument);
            if (!Parse.tryConsume(","))
                break;
        }
        Parse.consume(")");
        let result = new CallExpression(name, name.text, typeArguments, argumentList);
        return result;
    }

    static isCallExpression()
    {
        return Parse.instance.lexer.testScope(() => {
            Parse.consumeKind("identifier");
            Parse.parseTypeArguments();
            Parse.consume("(");
        });
    }

    static emitIncrement(token: LexerToken, old: Node1, extraArg?: Node1)
    {
        let args = [old];
        if (extraArg)
            args.push(extraArg);

        let name = "operator" + token.text;
        if (/=$/.test(name))
            name = (<RegExpType><unknown>RegExp).leftContext;

        if (name == "operator")
            throw new Error("Invalid name: " + name);

        return new CallExpression(token, name, [], args);
    }

    static finishParsingPostIncrement(token: LexerToken, left: Expression)
    {
        let readModifyWrite = new ReadModifyWriteExpression(token, left);
        readModifyWrite.newValueExp = Parse.emitIncrement(token, readModifyWrite.oldValueRef());
        readModifyWrite.resultExp = readModifyWrite.oldValueRef();
        return readModifyWrite;
    }

    static parseSuffixOperator(left: Expression, ...acceptableOperators: string[])
    {
        let token: LexerToken;
        while (token = Parse.tryConsume(...acceptableOperators)) {
            switch (token.text) {
            case "++":
            case "--":
                return Parse.finishParsingPostIncrement(token, left);
            case ".":
            case "->":
                if (token.text == "->")
                    left = new DereferenceExpression(token, left);
                left = new DotExpression(token, left, Parse.consumeKind("identifier").text);
                break;
            case "[": {
                let index = Parse.parseExpression();
                Parse.consume("]");
                left = new IndexExpression(token, left, index);
                break;
            }
            default:
                throw new Error("Bad token: " + token);
            }
        }
        return left;
    }

    static parsePossibleSuffix()
    {
        let acceptableOperators = ["++", "--", ".", "->", "["];
        let limitedOperators = [".", "->", "["];
        let left: Expression;
        if (Parse.isCallExpression()) {
            left = Parse.parseCallExpression();
            return Parse.parseSuffixOperator(left, ".", "->", "[");
            // acceptableOperators = limitedOperators; XTS
        } else {
            left = Parse.parseTerm();
            return Parse.parseSuffixOperator(left, "++", "--", ".", "->", "[");
        }
    }

    static finishParsingPreIncrement(token: LexerToken, left: Expression, extraArg?: Node1)
    {
        let readModifyWrite = new ReadModifyWriteExpression(token, left);
        readModifyWrite.newValueExp = Parse.emitIncrement(token, readModifyWrite.oldValueRef(), extraArg);
        readModifyWrite.resultExp = readModifyWrite.newValueRef();
        return readModifyWrite;
    }

    static parsePreIncrement()
    {
        let token = Parse.consume("++", "--");
        let left = Parse.parsePossiblePrefix();
        return Parse.finishParsingPreIncrement(token, left);
    }

   static parsePossiblePrefix(): Expression
    {
        let token: LexerToken;
        if (Parse.test("++", "--"))
            return Parse.parsePreIncrement();
        if (token = Parse.tryConsume("+", "-", "~"))
            return new CallExpression(token, "operator" + token.text, [], [Parse.parsePossiblePrefix()]);
        if (token = Parse.tryConsume("*"))
            return new DereferenceExpression(token, Parse.parsePossiblePrefix());
        if (token = Parse.tryConsume("&"))
            return new MakePtrExpression(token, Parse.parsePossiblePrefix());
        if (token = Parse.tryConsume("@"))
            return new MakeArrayRefExpression(token, Parse.parsePossiblePrefix());
        if (token = Parse.tryConsume("!")) {
            let remainder = Parse.parsePossiblePrefix();
            return new LogicalNot(token, new CallExpression(remainder.origin, "bool", [], [remainder]));
        }
        return Parse.parsePossibleSuffix();
    }

    static parsePossibleProduct()
    {
        return Parse.parseLeftOperatorCall(["*", "/", "%"], Parse.parsePossiblePrefix);
    }

    static parsePossibleSum()
    {
        return Parse.parseLeftOperatorCall(["+", "-"], Parse.parsePossibleProduct);
    }

   static parsePossibleShift()
    {
        return Parse.parseLeftOperatorCall(["<<", ">>"], Parse.parsePossibleSum);
    }

    static parsePossibleRelationalInequality()
    {
        return Parse.parseLeftOperatorCall(["<", ">", "<=", ">="], Parse.parsePossibleShift);
    }

    static parsePossibleRelationalEquality()
    {
        return Parse.genericParseLeft(
            ["==", "!="], Parse.parsePossibleRelationalInequality,
            (token, left, right) => {
                let result: Expression = new CallExpression(token, "operator==", [], [left, right]);
                if (token.text == "!=")
                    result = new LogicalNot(token, <CallExpression>result);
                return result;
            });
    }

    static parsePossibleBitwiseAnd()
    {
        return Parse.parseLeftOperatorCall(["&"], Parse.parsePossibleRelationalEquality);
    }

    static parsePossibleBitwiseXor()
    {
        return Parse.parseLeftOperatorCall(["^"], Parse.parsePossibleBitwiseAnd);
    }

    static parsePossibleBitwiseOr()
    {
        return Parse.parseLeftOperatorCall(["|"], Parse.parsePossibleBitwiseXor);
    }

    static parseLeftLogicalExpression(texts: string[], nextParser: ()=>Expression)
    {
        let emptyArray: Node1[] = [];
        return Parse.genericParseLeft(
            texts, nextParser,
            (token: LexerToken, left: Expression, right: Expression) => new LogicalExpression(token, token.text, new CallExpression(left.origin, "bool", emptyArray, [left]), new CallExpression(right.origin, "bool", emptyArray, [right])));
    }

    static parsePossibleLogicalAnd(): Expression
    {
        return Parse.parseLeftLogicalExpression(["&&"], Parse.parsePossibleBitwiseOr);
    }

    static parsePossibleLogicalOr()
    {
        return Parse.parseLeftLogicalExpression(["||"], Parse.parsePossibleLogicalAnd);
    }

    static parsePossibleTernaryConditional(): Expression
    {
        let predicate = Parse.parsePossibleLogicalOr();
        let operator = Parse.tryConsume("?");
        if (!operator)
            return predicate;
        throw new Error("no TernaryExpression");
        // return new TernaryExpression(operator, predicate, parsePossibleAssignment(), parsePossibleAssignment());
    }

    static parsePossibleAssignment(mode?: string): Expression
    {
        let lhs = Parse.parsePossibleTernaryConditional();
        let operator = Parse.tryConsume("=", "+=", "-=", "*=", "/=", "%=", "^=", "|=", "&=");
        if (!operator) {
            if (mode && mode == "required")
                Parse.instance.lexer.fail("Expected assignment");
            return lhs;
        }
        if (operator.text == "=")
            return new Assignment(operator, lhs, Parse.parsePossibleAssignment());
        return Parse.finishParsingPreIncrement(operator, lhs, Parse.parsePossibleAssignment());
    }

    static parseAssignment()
    {
        return Parse.parsePossibleAssignment("required");
    }

    static parsePostIncrement()
    {
        let left = Parse.parseSuffixOperator(Parse.parseTerm(), ".", "->", "[");
        let token = Parse.consume("++", "--");
        return Parse.finishParsingPostIncrement(token, left);
    }

    static parseEffectfulExpression(): Expression
    {
        if (Parse.isCallExpression())
            return Parse.parseCallExpression();
        let preIncrement = <Expression>Parse.instance.lexer.backtrackingScope(Parse.parsePreIncrement);
        if (preIncrement)
            return preIncrement;
        let postIncrement = <Expression>Parse.instance.lexer.backtrackingScope(Parse.parsePostIncrement);
        if (postIncrement)
            return postIncrement;
        return Parse.parseAssignment();
    }

    static genericParseCommaExpression(finalExpressionParser: (mode?: string)=>Expression)
    {
        let list: Expression[] = [];
        let origin = Parse.instance.lexer.peek();
        if (!origin)
            Parse.instance.lexer.fail("Unexpected end of file");
        for (;;) {
            let effectfulExpression = <Expression>Parse.instance.lexer.backtrackingScope(() => {
                // XTS
                let result: Expression;
                result = Parse.parseEffectfulExpression();
                Parse.consume(",");
                return result;
            });
            if (!effectfulExpression) {
                let final = finalExpressionParser();
                list.push(final);
                break;
            }
            list.push(effectfulExpression);
        }
        if (!list.length)
            throw new Error("Length should never be zero");
        if (list.length == 1)
            return list[0];
        return new CommaExpression(origin, list);
    }

    static parseCommaExpression(): Expression
    {
        return Parse.genericParseCommaExpression(Parse.parsePossibleAssignment);
    }

    static parseExpression()
    {
        return Parse.parseCommaExpression();
    }

    static parseEffectfulStatement()
    {
        let result = Parse.genericParseCommaExpression(Parse.parseEffectfulExpression);
        Parse.consume(";");
        return result;
    }

    static parseReturn()
    {
        let origin = Parse.consume("return");
        if (Parse.tryConsume(";"))
            return new Return(origin, null);
        let expression = Parse.parseExpression();
        Parse.consume(";");
        return new Return(origin, expression);
    }

    static parseBreak()
    {
        let origin = Parse.consume("break");
        Parse.consume(";");
        return new Break(origin);
    }

    static parseContinue()
    {
        let origin = Parse.consume("continue");
        Parse.consume(";");
        return new Continue(origin);
    }

    static parseIfStatement()
    {
        let origin = Parse.consume("if");
        Parse.consume("(");
        let conditional = Parse.parseExpression();
        Parse.consume(")");
        let body = Parse.parseStatement();
        let elseBody: Node1;
        if (Parse.tryConsume("else"))
            elseBody = Parse.parseStatement();
        return new IfStatement(origin, new CallExpression(conditional.origin, "bool", [], [conditional]), body, elseBody);
    }

    static parseWhile(): WhileLoop
    {
        let origin = Parse.consume("while");
        Parse.consume("(");
        let conditional = Parse.parseExpression();
        Parse.consume(")");
        let body = Parse.parseStatement();
        return new WhileLoop(origin, new CallExpression(conditional.origin, "bool", [], [conditional]), body);
    }

    static parseFor()
    {
        let origin = Parse.consume("for");
        Parse.consume("(");
        let initialization: Node1 | void;
        if (Parse.tryConsume(";"))
            initialization = undefined;
        else {
            initialization = Parse.instance.lexer.backtrackingScope(Parse.parseVariableDecls);
            if (!initialization)
                initialization = Parse.parseEffectfulStatement();
        }
        let conditionToken = Parse.tryConsume(";");
        let condition: Expression;
        if (conditionToken)
            condition = undefined;
        else {
            condition = Parse.parseExpression();
            Parse.consume(";");
            condition = new CallExpression(condition.origin, "bool", [], [condition]);
        }
        let increment: Expression;
        if (Parse.tryConsume(")"))
            increment = undefined;
        else {
            increment = Parse.parseExpression();
            Parse.consume(")");
        }
        let body = Parse.parseStatement();
        return new ForLoop(origin, <Expression>initialization, <CallExpression>condition, increment, body);
    }

    static parseDo()
    {
        let origin = Parse.consume("do");
        let body = Parse.parseStatement();
        Parse.consume("while");
        Parse.consume("(");
        let conditional = Parse.parseExpression();
        Parse.consume(")");
        return new DoWhileLoop(origin, body, new CallExpression(conditional.origin, "bool", [], [conditional]));
    }

    static parseVariableDecls()
    {
        let type = Parse.parseType();
        let list = [];
        do {
            let name = Parse.consumeKind("identifier");
            let initializer = Parse.tryConsume("=") ? Parse.parseExpression() : null;
            list.push(new VariableDecl(name, name.text, type, initializer));
        } while (Parse.consume(",", ";").text == ",");
        return new CommaExpression(type.origin, list);
    }

    static parseSwitchCase()
    {
        let token = Parse.consume("default", "case");
        let value: Value;
        if (token.text == "case")
            value = Parse.parseConstexpr();
        Parse.consume(":");
        let body = Parse.parseBlockBody("}", "default", "case");
        return new SwitchCase(token, value, body);
    }

    static parseSwitchStatement()
    {
        let origin = Parse.consume("switch");
        Parse.consume("(");
        let value = Parse.parseExpression();
        Parse.consume(")");
        Parse.consume("{");
        let result = new SwitchStatement(origin, value);
        while (!Parse.tryConsume("}"))
            result.add(Parse.parseSwitchCase());
        return result;
    }

    static parseStatement(): Node1
    {
        let token: LexerToken = Parse.instance.lexer.peek();
        if (token.text == ";") {
            Parse.instance.lexer.next();
            return null;
        }
        if (token.text == "return")
            return Parse.parseReturn();
        if (token.text == "break")
            return Parse.parseBreak();
        if (token.text == "continue")
            return Parse.parseContinue();
        if (token.text == "while")
            return Parse.parseWhile();
        if (token.text == "do")
            return Parse.parseDo();
        if (token.text == "for")
            return Parse.parseFor();
        if (token.text == "if")
            return Parse.parseIfStatement();
        if (token.text == "switch")
            return Parse.parseSwitchStatement();
        if (token.text == "trap") {
            let origin: LexerToken = Parse.consume("trap");
            Parse.consume(";");
            return new TrapStatement(origin);
        }
        if (token.text == "{")
            return Parse.parseBlock();
        let variableDecl = Parse.instance.lexer.backtrackingScope(Parse.parseVariableDecls);
        if (variableDecl)
            return variableDecl;
        return Parse.parseEffectfulStatement();
    }

    static parseBlockBody(...terminators: string[])
    {
        let block = new Block(Parse.instance.origin);
        while (!Parse.test(...terminators)) {
            let statement = Parse.parseStatement();
            if (statement)
                block.add(statement);
        }
        return block;
    }

    static parseBlock()
    {
        let origin = Parse.consume("{");
        let block = Parse.parseBlockBody("}");
        Parse.consume("}");
        return block;
    }

    static parseParameter()
    {
        let type = Parse.parseType();
        let name = Parse.tryConsumeKind("identifier");
        return new FuncParameter(type.origin, name ? name.text : null, type);
    }

    static parseParameters()
    {
        Parse.consume("(");
        let parameters = [];
        while (!Parse.test(")")) {
            parameters.push(Parse.parseParameter());
            if (!Parse.tryConsume(","))
                break;
        }
        Parse.consume(")");
        return parameters;
    }

    static parseFuncName()
    {
        if (Parse.tryConsume("operator")) {
            let token = Parse.consume("+", "-", "*", "/", "%", "^", "&", "|", "<", ">", "<=", ">=", "==", "++", "--", ".", "~", "<<", ">>", "[");
            if (token.text == "&") {
                if (Parse.tryConsume("[")) {
                    Parse.consume("]");
                    return "operator&[]";
                }
                if (Parse.tryConsume("."))
                    return "operator&." + Parse.consumeKind("identifier").text;
                return "operator&";
            }
            if (token.text == ".") {
                let result = "operator." + Parse.consumeKind("identifier").text;
                if (Parse.tryConsume("="))
                    result += "=";
                return result;
            }
            if (token.text == "[") {
                Parse.consume("]");
                let result = "operator[]";
                if (Parse.tryConsume("="))
                    result += "=";
                return result;
            }
            return "operator" + token.text;
        }
        return Parse.consumeKind("identifier").text;
    }

    static parseFuncDecl()
    {
        let origin: Origin;
        let returnType: Type;
        let name: string;
        let typeParameters: TypeParameters[];
        let isCast: boolean;
        let shaderType: string;
        let operatorToken: LexerToken = Parse.tryConsume("operator");
        if (operatorToken) {
            origin = operatorToken;
            typeParameters = Parse.parseTypeParameters();
            returnType = Parse.parseType();
            name = "operator cast";
            isCast = true;
        } else {
            let shaderTypeToken = Parse.tryConsume("vertex", "fragment");
            returnType = Parse.parseType();
            if (shaderTypeToken) {
                origin = shaderTypeToken;
                shaderType = shaderTypeToken.text;
            } else
                origin = returnType.origin;
            name = Parse.parseFuncName();
            typeParameters = Parse.parseTypeParameters();
            isCast = false;
        }
        let parameters = Parse.parseParameters();
        return new Func(origin, name, returnType, typeParameters, parameters, isCast, shaderType);
    }

    static parseProtocolFuncDecl()
    {
        let func = Parse.parseFuncDecl();
        return new ProtocolFuncDecl(func.origin, func.name, func.returnType, func.typeParameters, func.parameters, func.isCast, func.shaderType);
    }

    static parseFuncDef()
    {
        let func = Parse.parseFuncDecl();
        let body = Parse.parseBlock();
        return new FuncDef(func.origin, func.name, func.returnType, func.typeParameters, func.parameters, body, func.isCast, func.shaderType);
    }

    static parseProtocolDecl()
    {
        let origin = Parse.consume("protocol");
        let name = Parse.consumeKind("identifier").text;
        let result = new ProtocolDecl(origin, name);
        if (Parse.tryConsume(":")) {
            while (!Parse.test("{")) {
                result.addExtends(Parse.parseProtocolRef());
                if (!Parse.tryConsume(","))
                    break;
            }
        }
        Parse.consume("{");
        while (!Parse.tryConsume("}")) {
            result.add(Parse.parseProtocolFuncDecl());
            Parse.consume(";");
        }
        return result;
    }

    static parseField()
    {
        let type = Parse.parseType();
        let name = Parse.consumeKind("identifier");
        Parse.consume(";");
        return new Field(name, name.text, type);
    }

    static parseStructType()
    {
        let origin = Parse.consume("struct");
        let name = Parse.consumeKind("identifier").text;
        let typeParameters = Parse.parseTypeParameters();
        let result = new StructType(origin, name, typeParameters);
        Parse.consume("{");
        while (!Parse.tryConsume("}"))
            result.add(Parse.parseField());
        return result;
    }

    static parseNativeFunc()
    {
        let func = Parse.parseFuncDecl();
        Parse.consume(";");
        return new NativeFunc(func.origin, func.name, func.returnType, func.typeParameters, func.parameters, func.isCast, func.shaderType);
    }

    static parseNative()
    {
        let origin = Parse.consume("native");
        if (Parse.tryConsume("typedef")) {
            let name = Parse.consumeKind("identifier");
            let parameters = Parse.parseTypeParameters();
            Parse.consume(";");
            return new NativeType(origin, name.text, parameters);
        }
        return Parse.parseNativeFunc();
    }

    static parseRestrictedFuncDef()
    {
        Parse.consume("restricted");
        let result: Func;
        if (Parse.tryConsume("native"))
            result = Parse.parseNativeFunc();
        else
            result = Parse.parseFuncDef();
        result.isRestricted = true;
        return result;
    }

    static parseEnumMember()
    {
        let name = Parse.consumeKind("identifier");
        let value: Expression = null;
        if (Parse.tryConsume("="))
            value = Parse.parseConstexpr();
        return new EnumMember(name, name.text, value);
    }

    static parseEnumType()
    {
        Parse.consume("enum");
        let name = Parse.consumeKind("identifier");
        let baseType: Type;
        if (Parse.tryConsume(":"))
            baseType = Parse.parseType();
        else
            baseType = new TypeRef(name, "int", []);
        Parse.consume("{");
        let result = new EnumType(name, name.text, baseType);
        while (!Parse.test("}")) {
            result.add(Parse.parseEnumMember());
            if (!Parse.tryConsume(","))
                break;
        }
        Parse.consume("}");
        return result;
    }
}

function parse(program: Program, origin: string, originKind: string, lineNumberOffset: int, text: string)
{
    let parser = new Parse(program, origin, originKind, lineNumberOffset, text);
    // The hardest part of dealing with C-like languages is parsing variable declaration statements.
    // Let's consider if this happens in WSL. Here are the valid statements in WSL that being with an
    // identifier, if we assume that any expression can be a standalone statement.
    //
    //     x;
    //     x <binop> y;
    //     x < y;
    //     x < y > z;
    //     x = y;
    //     x.f = y;
    //     \exp = y;
    //     x[42] = y;
    //     x();
    //     x<y>();
    //     x y;
    //     x<y> z;
    //     device x[] y;
    //     x[42] y;
    //     device x^ y;
    //     thread x^^ y;
    //     x^thread^thread y;
    //     x^device^thread y;
    //
    // This has two problem areas:
    //
    //     - x<y>z can parse two different ways (as (x < y) > z or x<y> z).
    //     - x[42] could become either an assignment or a variable declaration.
    //     - x<y> could become either an assignment or a variable declaration.
    //
    // We solve the first problem by forbidding expressions as statements. The lack of function
    // pointers means that we can still allow function call statements - unlike in C, those cannot
    // have arbitrary expressions as the callee. The remaining two problems are solved by
    // backtracking. In all other respects, this is a simple recursive descent parser.
    for (;;) {
        let token = parser.lexer.peek();
        if (!token)
            return;
        if (token.text == ";")
            parser.lexer.next();
        else if (token.text == "typedef")
            program.add(Parse.parseTypeDef());
        else if (originKind == "native" && token.text == "native")
            program.add(Parse.parseNative());
        else if (originKind == "native" && token.text == "restricted")
            program.add(Parse.parseRestrictedFuncDef());
        else if (token.text == "struct")
            program.add(Parse.parseStructType());
        else if (token.text == "enum")
            program.add(Parse.parseEnumType());
        else if (token.text == "protocol")
            program.add(Parse.parseProtocolDecl());
        else
            program.add(Parse.parseFuncDef());
    }
}

let prepare = (() => {
    let standardProgram: Program;
    return function(origin?: string, lineNumberOffset?: int, text?: string) {
        if (!standardProgram) {
            standardProgram = new Program();
            let firstLineOfStandardLibrary = 28; // See StandardLibrary.js.
            parse(standardProgram, "/internal/stdlib", "native", firstLineOfStandardLibrary - 1, standardLibrary);
        }

        let program = cloneProgram(standardProgram);
        if (arguments.length) {
            parse(program, origin, "user", lineNumberOffset, text);
            program = programWithUnnecessaryThingsRemoved(program);
        }

        foldConstexprs(program);
        let nameResolver = createNameResolver(program);
        resolveNamesInTypes(program, nameResolver);
        resolveNamesInProtocols(program, nameResolver);
        resolveTypeDefsInTypes(program);
        resolveTypeDefsInProtocols(program);
        checkRecursiveTypes(program);
        synthesizeStructAccessors(program);
        synthesizeEnumFunctions(program);
        resolveNamesInFunctions(program, nameResolver);
        resolveTypeDefsInFunctions(program);

        flattenProtocolExtends(program);
        check(program);
        checkLiteralTypes(program);
        resolveProperties(program);
        findHighZombies(program);
        checkLiteralTypes(program);
        checkProgramWrapped(program);
        checkReturns(program);
        checkUnreachableCode(program);
        checkLoops(program);
        checkRecursion(program);
        checkProgramWrapped(program);
        findHighZombies(program);
        inline(program);

        return program;
    };
})();

class Program extends Node1 {
    intrinsics: Intrinsics;
    _funcInstantiator: FuncInstantiator;
    _functions: Map<string, Func[]>;
    _topLevelStatements: Node1[];
    _types: Map<string, Type>;
    _protocols: Map<string, Protocol>;
    _globalNameContext: NameContext;
    constructor()
    {
        super();
        this._topLevelStatements = [];
        this._functions = new Map();
        this._types = new Map();
        this._protocols = new Map();
        this._funcInstantiator = new FuncInstantiator(this);
        this._globalNameContext = new NameContext();
        this._globalNameContext.program = this;
        this._globalNameContext.recognizeIntrinsics();
        this.intrinsics = this._globalNameContext.intrinsics;
    }

    get topLevelStatements() { return this._topLevelStatements; }
    get functions() { return this._functions; }
    get types() { return this._types; }
    get protocols() { return this._protocols; }
    get funcInstantiator() { return this._funcInstantiator; }
    get globalNameContext() { return this._globalNameContext; }

    add(statement: Node1)
    {
        statement.program = this;
        if (statement instanceof Func) {
            let array = this._functions.get(statement.name);
            if (!array)
                this._functions.set(statement.name, array = []);
            array.push(statement);
        } else if (statement instanceof Type)
            this._types.set(statement.name, statement);
        else if (statement instanceof Protocol)
            this._protocols.set(statement.name, statement);
        else
            throw new Error("Statement is not a function or type: " + statement);
        this._topLevelStatements.push(statement);
        this._globalNameContext.add(statement);
    }

    toString()
    {
        if (!this._topLevelStatements.length)
            return "";
        return this._topLevelStatements.join(";") + ";";
    }
}

function programWithUnnecessaryThingsRemoved(program: Program)
{
    let nameFinder = new NameFinder();

    // Build our roots.
    for (let statement of program.topLevelStatements) {
        if (statement.origin.originKind === "user")
            nameFinder.add(statement.name);
    }

    // Unfortunately, we cannot know yet which operator casts we'll need.
    nameFinder.add("operator cast");

    // We need these even if the program doesn't mention them by name.
    nameFinder.add("void");
    nameFinder.add("bool");
    nameFinder.add("int");

    // Pull in things as necessary.
    while (nameFinder.worklist.length) {
        let name = nameFinder.worklist.pop();
        for (let thing of program.globalNameContext.underlyingThings(<object><unknown>Anything, name))
            thing.visit(nameFinder);
    }

    let result = new Program();
    for (let name of nameFinder.set) {
        for (let thing of program.globalNameContext.underlyingThings(<object><unknown>Anything, name))
            result.add(thing);
    }

    return result;
}

class RValueFinder {
    resolver_: PropertyResolver;
    constructor(resolver: PropertyResolver) {
        this.resolver_ = resolver;
    }

    visit(node: Node1) {
        node.visit(this.resolver_);
    }

    visitDotExpression(node: DotExpression)
    {
        node.struct1.visit(this.resolver_);
    }

    visitIndexExpression(node: IndexExpression)
    {
        node.array.visit(this.resolver_);
        this.visit(node.index);
    }

    visitVariableRef(node: VariableRef)
    {
    }

    visitDereferenceExpression(node: DereferenceExpression)
    {
        this.visit(node.ptr);
    }

    visitIdentityExpression(node: Node1)
    {
        node.target.visit(this.resolver_);
    }

    visitMakeArrayRefExpression(node: MakeArrayRefExpression)
    {
        this.visit(node.lValue);
    }
}

class PropertyResolver extends Visitor {
    _visitRValuesWithinLValue(node: Node1)
    {
        node.visit(new RValueFinder(this));
    }

    _visitPropertyAccess(node: PropertyAccessExpression)
    {
        let newNode = <Value>node.visit(new NormalUsePropertyResolver());
        newNode.visit(this);
        node.become(newNode);
    }

    visitDotExpression(node: DotExpression)
    {
        this._visitPropertyAccess(node);
    }

    visitIndexExpression(node: IndexExpression)
    {
        this._visitPropertyAccess(node);
    }

    _handleReadModifyWrite(node: ReadModifyWriteExpression)
    {
        let type = node.oldValueVar.type;
        if (!type)
            throw new Error("Null type");
        let simpleLHS = <Value>node.lValue.visit(new NormalUsePropertyResolver());
        if (simpleLHS.isLValue) {
            if (!simpleLHS.addressSpace)
                throw new Error(node.origin.originString + ": LHS without address space: " + simpleLHS);
            let ptrType = new PtrType(node.origin, simpleLHS.addressSpace, type);
            let ptrVar = new AnonymousVariable(node.origin, ptrType);
            node.become(new CommaExpression(node.origin, [
                node.oldValueVar, node.newValueVar, ptrVar,
                new Assignment(
                    node.origin, VariableRef.wrap(ptrVar),
                    new MakePtrExpression(node.origin, simpleLHS), ptrType),
                new Assignment(
                    node.origin, node.oldValueRef(),
                    new DereferenceExpression(
                        node.origin, VariableRef.wrap(ptrVar), type, simpleLHS.addressSpace),
                    type),
                new Assignment(node.origin, node.newValueRef(), node.newValueExp, type),
                new Assignment(
                    node.origin,
                    new DereferenceExpression(
                        node.origin, VariableRef.wrap(ptrVar), type, simpleLHS.addressSpace),
                    node.newValueRef(), type),
                node.resultExp
            ]));
            return;
        }

        node.lValue.hasBecome = false;
        let result = new ReadModifyWriteExpression(node.origin, (<PropertyAccessExpression>node.lValue).base, (<PropertyAccessExpression>node.lValue).baseType);
        result.newValueExp = new CommaExpression(node.origin, [
            node.oldValueVar, node.newValueVar,
            new Assignment(node.origin, node.oldValueRef(), (<PropertyAccessExpression>node.lValue).emitGet(result.oldValueRef()), type),
            new Assignment(node.origin, node.newValueRef(), node.newValueExp, type),
            (<PropertyAccessExpression>node.lValue).emitSet(result.oldValueRef(), node.newValueRef())
        ]);
        result.resultExp = node.newValueRef();
        this._handleReadModifyWrite(result);
        node.become(result);
    }

    visitReadModifyWriteExpression(node: ReadModifyWriteExpression)
    {
        node.newValueExp.visit(this);
        node.resultExp.visit(this);
        this._visitRValuesWithinLValue(node.lValue);
        this._handleReadModifyWrite(node);
    }

    visitAssignment(node: Assignment)
    {
        this._visitRValuesWithinLValue(node.lhs);
        node.rhs.visit(this);

        let simpleLHS = <Value>node.lhs.visit(new NormalUsePropertyResolver());
        if (simpleLHS.isLValue) {
            node.lhs.become(simpleLHS);
            return;
        }

        if (!(node.lhs instanceof PropertyAccessExpression))
            throw new Error("Unexpected lhs type: " + node.lhs.constructor.name);

        let result = new ReadModifyWriteExpression(node.origin, <PropertyAccessExpression>node.lhs.base, node.lhs.baseType);
        let resultVar = new AnonymousVariable(node.origin, node.type);
        result.newValueExp = new CommaExpression(node.origin, [
            resultVar,
            new Assignment(node.origin, VariableRef.wrap(resultVar), node.rhs, node.type),
            node.lhs.emitSet(result.oldValueRef(), VariableRef.wrap(resultVar))
        ]);
        result.resultExp = VariableRef.wrap(resultVar);
        this._handleReadModifyWrite(result);
        node.become(result);
    }

    visitMakePtrExpression(node: MakeArrayRefExpression)
    {
        super.visitMakePtrExpression(node);
        let value = <MakeArrayRefExpression|MakePtrExpression>node;
        if (!value.lValue.isLValue)
            throw new WTypeError(node.origin.originString, "Not an lvalue: " + node.lValue);
    }

    visitMakeArrayRefExpression(node: MakeArrayRefExpression)
    {
        super.visitMakeArrayRefExpression(node);
        if (!node.lValue.isLValue)
            throw new WTypeError(node.origin.originString, "Not an lvalue: " + node.lValue);
    }
}

class Protocol extends Node1 {
    _name: string;
    constructor(origin: Origin, name: string)
    {
        super();
        this._origin = origin;
        this._name = name;
    }

    get kind() { return Protocol; }

    toString()
    {
        return this.name;
    }
}

class ProtocolDecl extends Protocol {
    extends: ProtocolRef[];
    _signatures: ProtocolFuncDecl[];
    _signatureMap: Map<string, Func[]>;
    _typeVariable: TypeVariable;
    constructor(origin: Origin, name: string)
    {
        super(origin, name);
        this.extends = [];
        this._signatures = [];
        this._signatureMap = new Map();
        this._typeVariable = new TypeVariable(origin, name, null);
    }

    addExtends(protocol: ProtocolRef)
    {
        this.extends.push(protocol);
    }

    add(signature: ProtocolFuncDecl)
    {
        if (!(signature instanceof ProtocolFuncDecl))
            throw new Error("Signature isn't a ProtocolFuncDecl but a " + (<ProtocolFuncDecl>signature).constructor.name);

        signature.protocolDecl = this;
        this._signatures.push(signature);
        let overloads = this._signatureMap.get(signature.name);
        if (!overloads)
            this._signatureMap.set(signature.name, overloads = []);
        overloads.push(signature);
    }

    get signatures() { return this._signatures; }
    signaturesByName(name: string) { return this._signatureMap.get(name); }
    get typeVariable() { return this._typeVariable; }

    signaturesByNameWithTypeVariable(name: string, typeVariable: Node1)
    {
        let substitution = new Substitution([this.typeVariable], [typeVariable]);
        let result = this.signaturesByName(name);
        if (!result)
            return null;
        return result.map(signature => signature.visit(substitution));
    }

    inherits(otherProtocol: Protocol): VerityResultType
    {
        if (!otherProtocol)
            return {result: true, reason: undefined};

        if (otherProtocol instanceof ProtocolRef)
            otherProtocol = otherProtocol.protocolDecl;


        for (let otherSignature of (<ProtocolDecl>otherProtocol).signatures) {
            let signatures = this.signaturesByName(otherSignature.name);
            if (!signatures)
                return {result: false, reason: "Protocol " + this.name + " does not have a function named " + otherSignature.name + " (looking at signature " + otherSignature + ")"};
            let overload = <OverLoad>resolveOverloadImpl(
                signatures, [],
                otherSignature.parameterTypes,
                otherSignature.returnTypeForOverloadResolution);
            if (!overload.func)
                return {result: false, reason: "Did not find matching signature for " + otherSignature + " in " + this.name + ((<OverLoadFailure>overload).failures.length ? " (tried: " + (<OverLoadFailure>overload).failures.join("; ") + ")" : "")};
            let substitutedReturnType =
                overload.func.returnType.substituteToUnification(
                    overload.func.typeParameters, (<OverLoadSuccess>overload).unificationContext);
            if (!substitutedReturnType.equals(otherSignature.returnType))
                return {result: false, reason: "Return type mismatch between " + otherSignature.returnType + " and " + substitutedReturnType};
        }
        return {result: true, reason: undefined};
    }

    hasHeir(type: Type): VerityResultType
    {
        let substitution = new Substitution([this._typeVariable], [type]);
        let signatures = this.signatures;
        for (let originalSignature of signatures) {
            let signature = <ProtocolFuncDecl>originalSignature.visit(substitution);
            let overload = <OverLoad>resolveOverloadImpl(signature.possibleOverloads, signature.typeParameters, signature.parameterTypes, signature.returnTypeForOverloadResolution);
            if (!overload.func)
                return {result: false, reason: "Did not find matching signature for " + originalSignature + " (at " + originalSignature.origin.originString + ") with type " + type + ((<OverLoadFailure>overload).failures.length ? " (tried: " + (<OverLoadFailure>overload).failures.join("; ") + ")" : "")};

            let substitutedReturnType = overload.func.returnType.substituteToUnification(
                overload.func.typeParameters, (<OverLoadSuccess>overload).unificationContext);
            if (!substitutedReturnType.equals(signature.returnType))
                return {result: false, reason: "At signature " + originalSignature + " (at " + originalSignature.origin.originString + "): return type mismatch between " + signature.returnType + " and " + substitutedReturnType + " in found function " + overload.func.toDeclString()};
        }
        return {result: true, reason: undefined};
    }

    toString()
    {
        return "protocol " + this.name + " { " + this.signatures.join("; ") + "; }";
    }
}

class ProtocolFuncDecl extends Func {
    protocolDecl: ProtocolDecl;
    possibleOverloads: Func[];
    get typeParametersForCallResolution()
    {
        return this.typeParameters.concat(this.protocolDecl.typeVariable);
    }
}

class ProtocolRef extends Protocol {
    protocolDecl: ProtocolDecl;
    constructor(origin: Origin, name: string)
    {
        super(origin, name);
        this.protocolDecl = null;
    }

    inherits(other: Protocol)
    {
        return this.protocolDecl.inherits(other);
    }

    hasHeir(type: Type)
    {
        return this.protocolDecl.hasHeir(type);
    }

    get isPrimitive()
    {
        return (<Type><unknown>this.protocolDecl).isPrimitive;
    }
}

class PtrType extends ReferenceType {
    constructor(origin: Origin, addressSpace: string, elementType: Type) {
        super(origin, addressSpace, elementType);
        this._isPtr = true;
    }

    unifyImpl(unificationContext: UnificationContext, other: Type)
    {
        if (!other.isPtr)
            return false;
        if (this.addressSpace != other.addressSpace)
            return false;
        return this.elementType.unify(unificationContext, other.elementType);
    }

    argumentForAndOverload(origin: Origin, value: ValueType): MakeArrayRefExpression
    {
        throw new WTypeError(origin.originString, "Pointer subscript is not valid");
    }
    argumentTypeForAndOverload(origin: Origin, type: Type): PtrType
    {
        throw new WTypeError(origin.originString, "Pointer subscript is not valid");
    }
    returnTypeFromAndOverload(origin: Origin): Type
    {
        return this.elementType;
    }

    toString()
    {
        return this.elementType + "* " + this.addressSpace;
    }
}

class ReadModifyWriteExpression extends Expression {
    oldValueVar: AnonymousVariable;
    newValueVar: AnonymousVariable;
    newValueExp: Expression;
    resultExp: Expression;
    _lValue: Expression;
    constructor(origin: Origin, lValue: Expression, type: Type = null)
    {
        super(origin);
        this._lValue = lValue;
        this.oldValueVar = new AnonymousVariable(origin, type);
        this.newValueVar = new AnonymousVariable(origin, type);
        this.newValueExp = null;
        this.resultExp = null;
    }

    get lValue() { return this._lValue; }

    oldValueRef() { return VariableRef.wrap(this.oldValueVar); }
    newValueRef() { return VariableRef.wrap(this.newValueVar); }

    toString()
    {
        return "RMW(" + this.lValue + ", " + this.oldValueVar + ", " + this.newValueVar + ", " + this.newValueExp + ", " + this.resultExp + ")";
    }
}

class RecursionChecker extends Visitor {
    _visiting: VisitingSet;
    constructor(program?: Program)
    {
        super();
        this._visiting = new VisitingSet();
    }

    visitFuncDef(node: FuncDef)
    {
        this._visiting.doVisit(node, () => super.visitFuncDef(node));
    }

    visitCallExpression(node: CallExpression)
    {
        node.func.visit(this);
    }
}

class RecursiveTypeChecker extends Visitor {
    _visiting: VisitingSet;
    constructor()
    {
        super();
        this._visiting = new VisitingSet();
    }

    visitFuncDef(node: FuncDef)
    {
    }

    visitNativeFunc(node: NativeFunc)
    {
    }

    visitStructType(node: StructType): undefined
    {
        this._visiting.doVisit(node, () => super.visitStructType(node));
        return undefined;
    }

    visitReferenceType(node: ReferenceType)
    {
    }

    visitTypeRef(node: TypeRef)
    {
        super.visitTypeRef(node);
        node.type.visit(this);
    }
}

function createNameResolver(program: Program)
{
    return new NameResolver(program.globalNameContext);
}

function resolveNamesInTypes(program: Program, nameResolver: NameResolver)
{
    for (let type of program.types.values())
        nameResolver.doStatement(type);
}

function resolveNamesInProtocols(program: Program, nameResolver: NameResolver)
{
    for (let protocol of program.protocols.values())
        nameResolver.doStatement(protocol);
}

function resolveNamesInFunctions(program: Program, nameResolver: NameResolver)
{
    for (let funcs of program.functions.values()) {
        for (let func of funcs)
            nameResolver.doStatement(func);
    }
}

function resolveOverloadImpl(functions: Func[], typeArguments: Node1[], argumentTypes: Type[], returnType: Type, allowEntryPoint: boolean = false): OverLoad
{
    if (!functions)
        throw new Error("Null functions; that should have been caught by the caller.");

    let failures = [];
    let successes: OverLoadSuccess[] = [];
    for (let func of functions) {
        if (!allowEntryPoint && func.shaderType) {
            failures.push(new OverloadResolutionFailure(func, "Function is a " + func.shaderType + " shader, so it cannot be called from within an existing shader."))
            continue;
        }
        let overload = inferTypesForCall(func, typeArguments, argumentTypes, returnType);
        if (overload.failure)
            failures.push(overload.failure);
        else
            successes.push(<OverLoadSuccess>overload);
    }

    if (!successes.length)
        return <OverLoadFailure>{failures: failures, func: undefined};

    let minimumConversionCost = successes.reduce(
        (result, overload) => Math.min(result, overload.unificationContext.conversionCost),
        Infinity);
    successes = successes.filter(
        overload => overload.unificationContext.conversionCost == minimumConversionCost);

    // If any of the signatures are restricted then we consider those first. This is an escape mechanism for
    // built-in things.
    // FIXME: It should be an error to declare a function that is at least as specific as a restricted function.
    // https://bugs.webkit.org/show_bug.cgi?id=176580
    let hasRestricted = successes.reduce(
        (result, overload) => result || overload.func.isRestricted,
        false);
    if (hasRestricted)
        successes = successes.filter(overload => overload.func.isRestricted);

    // We are only interested in functions that are at least as specific as all of the others. This means
    // that they can be "turned around" and applied onto all of the other functions in the list.
    let prunedSuccesses = [];
    for (let i = 0; i < successes.length; ++i) {
        let ok = true;
        let argumentFunc = successes[i].func;
        for (let j = 0; j < successes.length; ++j) {
            if (i == j)
                continue;
            let parameterFunc = successes[j].func;
            let overload = inferTypesForCall(
                parameterFunc,
                typeArguments.length ? argumentFunc.typeParameters : [],
                argumentFunc.parameterTypes,
                argumentFunc.returnTypeForOverloadResolution);
            if (!overload.func) {
                ok = false;
                break;
            }
        }
        if (ok)
            prunedSuccesses.push(successes[i]);
    }

    if (prunedSuccesses.length == 1)
        return prunedSuccesses[0];

    let ambiguityList: OverLoadSuccess[];
    let message: string;
    if (prunedSuccesses.length == 0) {
        ambiguityList = successes;
        message = "Ambiguous overload - no function can be applied to all others";
    } else {
        ambiguityList = prunedSuccesses;
        message = "Ambiguous overload - functions mutually applicable";
    }

    return {failures: ambiguityList.map(overload => new OverloadResolutionFailure(overload.func, message)), func: undefined};
}

function resolveProperties(program: Program)
{
    program.visit(new PropertyResolver());
}


function resolveTypeDefsInTypes(program: Program)
{
    let resolver = new TypeDefResolver();
    for (let type of program.types.values())
        type.visit(resolver);
}

function resolveTypeDefsInProtocols(program: Program)
{
    let resolver = new TypeDefResolver();
    for (let protocol of program.protocols.values())
        (<Node1>protocol).visit(resolver);
}

function resolveTypeDefsInFunctions(program: Program)
{
    let resolver = new TypeDefResolver();
    for (let funcs of program.functions.values()) {
        for (let func of funcs)
            func.visit(resolver);
    }
}

class Return extends Node1 {
    func: Func;
    _value: Value;
    constructor(origin: Origin, value: Value)
    {
        super();
        this._origin = origin;
        this._value = value;
    }

    get value() { return this._value; }

    toString()
    {
        if (this.value)
            return "return " + this.value;
        return "return";
    }
};

class ReturnChecker extends Visitor {
    _program: Program;
    returnStyle: {DefinitelyReturns: string, DefinitelyDoesntReturn: string, HasntReturnedYet: string};
    constructor(program: Program)
    {
        super();
        this.returnStyle = {
            DefinitelyReturns: "Definitely Returns",
            DefinitelyDoesntReturn: "Definitely Doesn't Return",
            HasntReturnedYet: "Hasn't Returned Yet"
        };
        this._program = program;
    }

    _mergeReturnStyle(a: string, b: string)
    {
        if (!a)
            return b;
        if (!b)
            return a;
        switch (a) {
        case this.returnStyle.DefinitelyReturns:
        case this.returnStyle.DefinitelyDoesntReturn:
            if (a == b)
                return a;
            return this.returnStyle.HasntReturnedYet;
        case this.returnStyle.HasntReturnedYet:
            return this.returnStyle.HasntReturnedYet;
        default:
            throw new Error("Bad return style: " + a);
        }
    }

    visitFuncDef(node: FuncDef)
    {
        if (node.returnType.equals(node.program.intrinsics.void))
            return;

        let bodyValue = node.body.visit(this);
        if (bodyValue == this.returnStyle.DefinitelyDoesntReturn || bodyValue == this.returnStyle.HasntReturnedYet)
            throw new WTypeError(node.origin.originString, "Function does not return");
    }

    visitBlock(node: Block)
    {
        for (let statement of node.statements) {
            switch (statement.visit(this)) {
            case this.returnStyle.DefinitelyReturns:
                return this.returnStyle.DefinitelyReturns;
            case this.returnStyle.DefinitelyDoesntReturn:
                return this.returnStyle.DefinitelyDoesntReturn;
            case this.returnStyle.HasntReturnedYet:
                continue;
            }
        }
        return this.returnStyle.HasntReturnedYet;
    }

    visitIfStatement(node: IfStatement)
    {
        if (node.elseBody) {
            let bodyValue = node.body.visit(this);
            let elseValue = node.elseBody.visit(this);
            return this._mergeReturnStyle(<string>bodyValue, <string>elseValue);
        }
        return this.returnStyle.HasntReturnedYet;
    }

    _isBoolCastFromLiteralTrue(node: CallExpression | FunctionLikeBlock)
    {
        return node.isCast && node.returnType instanceof TypeRef && node.returnType.equals(this._program.intrinsics.bool) && node.argumentList.length == 1 && node.argumentList[0] instanceof BoolLiteral && (<BoolLiteral>node.argumentList[0]).value;
    }

    visitWhileLoop(node: WhileLoop)
    {
        let bodyReturn = <string>node.body.visit(this);
        if (node.conditional instanceof CallExpression && this._isBoolCastFromLiteralTrue(node.conditional)) {
            switch (bodyReturn) {
            case this.returnStyle.DefinitelyReturns:
                return this.returnStyle.DefinitelyReturns;
            case this.returnStyle.DefinitelyDoesntReturn:
            case this.returnStyle.HasntReturnedYet:
                return this.returnStyle.HasntReturnedYet;
            }
        }
        return this.returnStyle.HasntReturnedYet;
    }

    visitDoWhileLoop(node: DoWhileLoop)
    {
        let result = this.returnStyle.HasntReturnedYet;
        switch (node.body.visit(this)) {
        case this.returnStyle.DefinitelyReturns:
            result = this.returnStyle.DefinitelyReturns;
        case this.returnStyle.DefinitelyDoesntReturn:
        case this.returnStyle.HasntReturnedYet:
            result = this.returnStyle.HasntReturnedYet;
        }
        return result;
    }

    visitForLoop(node: ForLoop)
    {
        let bodyReturn = node.body.visit(this);
        if (node.condition === undefined || this._isBoolCastFromLiteralTrue(node.condition)) {
            switch (bodyReturn) {
            case this.returnStyle.DefinitelyReturns:
                return this.returnStyle.DefinitelyReturns;
            case this.returnStyle.DefinitelyDoesntReturn:
            case this.returnStyle.HasntReturnedYet:
                return this.returnStyle.HasntReturnedYet;
            }
        }
    }

    visitSwitchStatement(node: SwitchStatement)
    {
        // FIXME: This seems like it's missing things. For example, we need to be smart about this:
        //
        // for (;;) {
        //     switch (...) {
        //     ...
        //         continue; // This continues the for loop
        //     }
        // }
        //
        // I'm not sure what that means for this analysis. I'm starting to think that the right way to
        // build this analysis is to run a visitor that builds a CFG and then analyze the CFG.
        // https://bugs.webkit.org/show_bug.cgi?id=177172

        let returnStyle: string = null;
        for (let switchCase of node.switchCases) {
            let bodyStyle = <string>switchCase.body.visit(this);
            // FIXME: The fact that we need this demonstrates the need for CFG analysis.
            if (bodyStyle == this.returnStyle.DefinitelyDoesntReturn)
                bodyStyle = this.returnStyle.HasntReturnedYet;
            returnStyle = this._mergeReturnStyle(returnStyle, bodyStyle);
        }
        return returnStyle;
    }

    visitReturn(node: Return)
    {
        return this.returnStyle.DefinitelyReturns;
    }

    visitTrapStatement(node: TrapStatement)
    {
        return this.returnStyle.DefinitelyReturns;
    }

    visitBreak(node: Break)
    {
        return this.returnStyle.DefinitelyDoesntReturn;
    }

    visitContinue(node: Continue)
    {
        // FIXME: This seems wrong. Consider a loop like:
        //
        // int foo() { for (;;) { continue; } }
        //
        // This program shouldn't claim that the problem is that it doesn't return.
        // https://bugs.webkit.org/show_bug.cgi?id=177172
        return this.returnStyle.DefinitelyDoesntReturn;
    }
}

class ReturnException {
    _value: Value;
    constructor(value: Value)
    {
        this._value = value;
    }

    get value() { return this._value; }
}

// NOTE: The next line is line 28, and we rely on this in Prepare.js.
let standardLibrary = `
// This is the WSL standard library. Implementations of all of these things are in
// Intrinsics.js.

// Need to bootstrap void first.
native typedef void;

native typedef uint8;
native typedef int32;
native typedef uint32;
native typedef bool;
typedef int = int32;
typedef uint = uint32;

native typedef float32;
native typedef float64;
typedef float = float32;
typedef double = float64;

native operator int32(uint32);
native operator int32(uint8);
native operator int32(float);
native operator int32(double);
native operator uint32(int32);
native operator uint32(uint8);
native operator uint32(float);
native operator uint32(double);
native operator uint8(int32);
native operator uint8(uint32);
native operator uint8(float);
native operator uint8(double);
native operator float(int32);
native operator float(uint32);
native operator float(uint8);
native operator float(double);
native operator double(float);
native operator double(int32);
native operator double(uint32);
native operator double(uint8);

native int operator+(int, int);
native uint operator+(uint, uint);
uint8 operator+(uint8 a, uint8 b) { return uint8(uint(a) + uint(b)); }
native float operator+(float, float);
native double operator+(double, double);
int operator++(int value) { return value + 1; }
uint operator++(uint value) { return value + 1; }
uint8 operator++(uint8 value) { return value + 1; }
native int operator-(int, int);
native uint operator-(uint, uint);
uint8 operator-(uint8 a, uint8 b) { return uint8(uint(a) - uint(b)); }
native float operator-(float, float);
native double operator-(double, double);
int operator--(int value) { return value - 1; }
uint operator--(uint value) { return value - 1; }
uint8 operator--(uint8 value) { return value - 1; }
native int operator*(int, int);
native uint operator*(uint, uint);
uint8 operator*(uint8 a, uint8 b) { return uint8(uint(a) * uint(b)); }
native float operator*(float, float);
native double operator*(double, double);
native int operator/(int, int);
native uint operator/(uint, uint);
uint8 operator/(uint8 a, uint8 b) { return uint8(uint(a) / uint(b)); }
native int operator&(int, int);
native int operator|(int, int);
native int operator^(int, int);
native int operator~(int);
native int operator<<(int, uint);
native int operator>>(int, uint);
native uint operator&(uint, uint);
native uint operator|(uint, uint);
native uint operator^(uint, uint);
native uint operator~(uint);
native uint operator<<(uint, uint);
native uint operator>>(uint, uint);
uint8 operator&(uint8 a, uint8 b) { return uint8(uint(a) & uint(b)); }
uint8 operator|(uint8 a, uint8 b) { return uint8(uint(a) | uint(b)); }
uint8 operator^(uint8 a, uint8 b) { return uint8(uint(a) ^ uint(b)); }
uint8 operator~(uint8 value) { return uint8(~uint(value)); }
uint8 operator<<(uint8 a, uint b) { return uint8(uint(a) << (b & 7)); }
uint8 operator>>(uint8 a, uint b) { return uint8(uint(a) >> (b & 7)); }
native float operator/(float, float);
native double operator/(double, double);
native bool operator==(int, int);
native bool operator==(uint, uint);
bool operator==(uint8 a, uint8 b) { return uint(a) == uint(b); }
native bool operator==(bool, bool);
native bool operator==(float, float);
native bool operator==(double, double);
native bool operator<(int, int);
native bool operator<(uint, uint);
bool operator<(uint8 a, uint8 b) { return uint(a) < uint(b); }
native bool operator<(float, float);
native bool operator<(double, double);
native bool operator<=(int, int);
native bool operator<=(uint, uint);
bool operator<=(uint8 a, uint8 b) { return uint(a) <= uint(b); }
native bool operator<=(float, float);
native bool operator<=(double, double);
native bool operator>(int, int);
native bool operator>(uint, uint);
bool operator>(uint8 a, uint8 b) { return uint(a) > uint(b); }
native bool operator>(float, float);
native bool operator>(double, double);
native bool operator>=(int, int);
native bool operator>=(uint, uint);
bool operator>=(uint8 a, uint8 b) { return uint(a) >= uint(b); }
native bool operator>=(float, float);
native bool operator>=(double, double);

bool operator&(bool a, bool b)
{
    if (a)
        return b;
    return false;
}

bool operator|(bool a, bool b)
{
    if (a)
        return true;
    if (b)
        return true;
    return false;
}

bool operator^(bool a, bool b)
{
    if (a)
        return !b;
    return b;
}

bool operator~(bool value)
{
    return !value;
}

protocol Addable {
    Addable operator+(Addable, Addable);
}

protocol Equatable {
    bool operator==(Equatable, Equatable);
}

restricted operator<T> T()
{
    T defaultValue;
    return defaultValue;
}

restricted operator<T> T(T x)
{
    return x;
}

operator<T:Equatable> bool(T x)
{
    return x != T();
}

struct vec2<T> {
    T x;
    T y;
}

typedef int2 = vec2<int>;
typedef uint2 = vec2<uint>;
typedef float2 = vec2<float>;
typedef double2 = vec2<double>;

operator<T> vec2<T>(T x, T y)
{
    vec2<T> result;
    result.x = x;
    result.y = y;
    return result;
}

bool operator==<T:Equatable>(vec2<T> a, vec2<T> b)
{
    return a.x == b.x && a.y == b.y;
}

thread T* operator&[]<T>(thread vec2<T>* foo, uint index)
{
    if (index == 0)
        return &foo->x;
    if (index == 1)
        return &foo->y;
    trap;
}

struct vec3<T> {
    T x;
    T y;
    T z;
}

typedef int3 = vec3<int>;
typedef uint3 = vec3<uint>;
typedef float3 = vec3<float>;
typedef double3 = vec3<double>;

operator<T> vec3<T>(T x, T y, T z)
{
    vec3<T> result;
    result.x = x;
    result.y = y;
    result.z = z;
    return result;
}

operator<T> vec3<T>(vec2<T> v2, T z)
{
    vec3<T> result;
    result.x = v2.x;
    result.y = v2.y;
    result.z = z;
    return result;
}

operator<T> vec3<T>(T x, vec2<T> v2)
{
    vec3<T> result;
    result.x = x;
    result.y = v2.x;
    result.z = v2.y;
    return result;
}

bool operator==<T:Equatable>(vec3<T> a, vec3<T> b)
{
    return a.x == b.x && a.y == b.y && a.z == b.z;
}

thread T* operator&[]<T>(thread vec3<T>* foo, uint index)
{
    if (index == 0)
        return &foo->x;
    if (index == 1)
        return &foo->y;
    if (index == 2)
        return &foo->z;
    trap;
}

struct vec4<T> {
    T x;
    T y;
    T z;
    T w;
}

typedef int4 = vec4<int>;
typedef uint4 = vec4<uint>;
typedef float4 = vec4<float>;
typedef double4 = vec4<double>;

operator<T> vec4<T>(T x, T y, T z, T w)
{
    vec4<T> result;
    result.x = x;
    result.y = y;
    result.z = z;
    result.w = w;
    return result;
}

operator<T> vec4<T>(vec2<T> v2, T z, T w)
{
    vec4<T> result;
    result.x = v2.x;
    result.y = v2.y;
    result.z = z;
    result.w = w;
    return result;
}

operator<T> vec4<T>(T x, vec2<T> v2, T w)
{
    vec4<T> result;
    result.x = x;
    result.y = v2.x;
    result.z = v2.y;
    result.w = w;
    return result;
}

operator<T> vec4<T>(T x, T y, vec2<T> v2)
{
    vec4<T> result;
    result.x = x;
    result.y = y;
    result.z = v2.x;
    result.w = v2.y;
    return result;
}

operator<T> vec4<T>(vec2<T> v2a, vec2<T> v2b)
{
    vec4<T> result;
    result.x = v2a.x;
    result.y = v2a.y;
    result.z = v2b.x;
    result.w = v2b.y;
    return result;
}

operator<T> vec4<T>(vec3<T> v3, T w)
{
    vec4<T> result;
    result.x = v3.x;
    result.y = v3.y;
    result.z = v3.z;
    result.w = w;
    return result;
}

operator<T> vec4<T>(T x, vec3<T> v3)
{
    vec4<T> result;
    result.x = x;
    result.y = v3.x;
    result.z = v3.y;
    result.w = v3.z;
    return result;
}

bool operator==<T:Equatable>(vec4<T> a, vec4<T> b)
{
    return a.x == b.x && a.y == b.y && a.z == b.z && a.w == b.w;
}

thread T* operator&[]<T>(thread vec4<T>* foo, uint index)
{
    if (index == 0)
        return &foo->x;
    if (index == 1)
        return &foo->y;
    if (index == 2)
        return &foo->z;
    if (index == 3)
        return &foo->w;
    trap;
}

native thread T* operator&[]<T>(thread T[], uint);
native threadgroup T* operator&[]<T>(threadgroup T[], uint);
native device T* operator&[]<T>(device T[], uint);
native constant T* operator&[]<T>(constant T[], uint);

native uint operator.length<T>(thread T[]);
native uint operator.length<T>(threadgroup T[]);
native uint operator.length<T>(device T[]);
native uint operator.length<T>(constant T[]);

uint operator.length<T, uint length>(T[length])
{
    return length;
}
`;

function intToString(x: int)
{
    switch (x) {
    case 0:
        return "x";
    case 1:
        return "y";
    case 2:
        return "z";
    case 3:
        return "w";
    default:
        throw new Error("Could not generate standard library.");
    }
}

// There are 481 swizzle operators. Let's not list them explicitly.
function _generateSwizzle(maxDepth: int, maxItems: int, array?: string[])
{
    if (!array)
        array = [];
    if (array.length == maxDepth) {
        let result = `vec${array.length}<T> operator.${array.join("")}<T>(vec${maxItems}<T> v)
{
    vec${array.length}<T> result;
`;
        for (let i = 0; i < array.length; ++i) {
            result += `    result.${intToString(i)} = v.${array[i]};
`;
        }
        result += `    return result;
}
`;
        return result;
    }
    let result = "";
    for (let i = 0; i < maxItems; ++i) {
        array.push(intToString(i));
        result += _generateSwizzle(maxDepth, maxItems, array);
        array.pop();
    }
    return result;
}

for (let maxDepth = 2; maxDepth <= 4; maxDepth++) {
    for (let maxItems = 2; maxItems <= 4; maxItems++)
        standardLibrary += _generateSwizzle(maxDepth, maxItems);
}

class StatementCloner extends Rewriter {
    visitFuncDef(node: FuncDef): FuncDef
    {
        let typeParameters = node.typeParameters.map(typeParameter => typeParameter.visit(this));
        let result = new FuncDef(
            node.origin, node.name,
            <Type>node.returnType.visit(this),
            <TypeParameters[]>typeParameters,
            <FuncParameter[]>node.parameters.map(parameter => parameter.visit(this)),
            <Node1>node.body.visit(this),
            node.isCast, node.shaderType);
        result.isRestricted = node.isRestricted;
        return result;
    }

    visitNativeFunc(node: NativeFunc)
    {
        let result: NativeFunc = new NativeFunc(
            node.origin, node.name,
            <Type>node.returnType.visit(this),
            <TypeParameters[]>node.typeParameters.map(typeParameter => typeParameter.visit(this)),
            <FuncParameter[]>node.parameters.map(parameter => parameter.visit(this)),
            node.isCast, node.shaderType);
        result.isRestricted = node.isRestricted;
        return result;
    }

    visitNativeType(node: NativeType): NativeType
    {
        return new NativeType(
            node.origin, node.name, <TypeParameters[]>node.typeParameters.map(typeParameter => typeParameter.visit(this)));
    }

    visitTypeDef(node: TypeDef): TypeDef
    {
        return new TypeDef(
            node.origin, node.name,
            <Node1[]>node.typeParameters.map((typeParameter: Node1) => typeParameter.visit(this)),
            <Type>node.type.visit(this));
    }

    visitStructType(node: StructType): StructType
    {
        let result = new StructType(
            node.origin, node.name,
            <Node1[]>node.typeParameters.map((typeParameter: Node1) => typeParameter.visit(this)));
        for (let field of node.fields)
            result.add(<Field>field.visit(this));
        return result;
    }

    visitConstexprTypeParameter(node: ConstexprTypeParameter): ConstexprTypeParameter
    {
        return new ConstexprTypeParameter(node.origin, node.name, <Type>node.type.visit(this));
    }

    visitProtocolDecl(node: ProtocolDecl)
    {
        let result = new ProtocolDecl(node.origin, node.name);
        for (let protocol of node.extends)
            result.addExtends(<ProtocolRef>protocol.visit(this));
        for (let signature of node.signatures)
            result.add(<ProtocolFuncDecl>signature.visit(this));
        return result;
    }

    visitTypeVariable(node: TypeVariable): TypeVariable
    {
        return new TypeVariable(node.origin, node.name, <ProtocolRef>Node1.visit(node.protocol, this));
    }

    visitProtocolRef(node: ProtocolRef)
    {
        return new ProtocolRef(node.origin, node.name);
    }

    visitBoolLiteral(node: BoolLiteral)
    {
        return new BoolLiteral(node.origin, node.value);
    }

    visitTypeOrVariableRef(node: TypeOrVariableRef)
    {
        return new TypeOrVariableRef(node.origin, node.name);
    }

    visitEnumType(node: EnumType): EnumType
    {
        let result = new EnumType(node.origin, node.name, <Type>node.baseType.visit(this));
        for (let member of node.members)
            result.add(member);
        return result;
    }
}

class StructLayoutBuilder extends Visitor {
    _offset: int;
    constructor()
    {
        super();
        this._offset = 0;
    }

    visitReferenceType(node: ReferenceType)
    {
    }

    visitStructType(node: StructType): undefined
    {
        if (node.size != null)
            return;
        if (node.typeParameters.length)
            throw new Error("Cannot do layout for generic type: " + node);
        let oldOffset = this._offset;
        this._offset = 0;
        super.visitStructType(node);
        node.size = this._offset;
        this._offset += oldOffset;
    }

    get offset() { return this._offset; }

    visitField(node: Field)
    {
        super.visitField(node);
        node.offset = this._offset;
        let size = node.type.size;
        if (size == null)
            throw new Error("Type does not have size: " + node.type);
        this._offset += size;
    }

    visitNativeFuncInstance(node: NativeFuncInstance)
    {
        super.visitNativeFuncInstance(node);
        node.func.didLayoutStructsInImplementationData(node.implementationData);
    }

    visitTypeRef(node: TypeRef)
    {
        super.visitTypeRef(node);
        node.type.visit(this);
    }

    visitCallExpression(node: CallExpression)
    {
        for (let argument of node.argumentList)
            Node1.visit(argument, this);
        let handleTypeArguments = (actualTypeArguments: Node1[]) => {
            if (actualTypeArguments) {
                for (let argument of actualTypeArguments)
                    (<Node1>argument).visit(this);
            }
        };
        handleTypeArguments(node.instantiatedActualTypeArguments);
        Node1.visit(node.nativeFuncInstance, this);
        Node1.visit(node.resultType, this);
    }
}

function StructTypePopulate(buffer: EBuffer, offset: int) {
    if (this.size == null)
        throw new Error("Struct does not have layout: " + this + " "); //+ describe(this));
    for (let field of this.fields)
        field.type.populateDefaultValue(buffer, offset + field.offset);
}

class StructType extends Type {
    _name: string;
    _typeParameters: Node1[];
    _fields: Map<string, Field>;
    constructor(origin: Origin, name: string, typeParameters: Node1[])
    {
        super();
        this._origin = origin;
        this._name = name;
        this._typeParameters = typeParameters;
        this._fields = new Map();
        this.instantiate = this._instantiate;
        this.populateDefaultValue = StructTypePopulate;
    }

    _instantiate(typeArguments: Node1[]){
        let substitution: Substitution = null;
        let typeParameters = this.typeParameters;

        if (typeArguments) {
            if (typeArguments.length != this.typeParameters.length)
                throw new WTypeError(this.origin.originString, "Wrong number of type arguments to instantiation");

            substitution = new Substitution(this.typeParameters, typeArguments);
            typeParameters = [];
        }

        let instantiateImmediates = new InstantiateImmediates();
        let result = new StructType(this.origin, this.name, typeParameters);
        for (let field of this.fields) {
            let newField = field;
            if (substitution)
                newField = <Field>newField.visit(substitution);
            newField = <Field>newField.visit(instantiateImmediates);
            result.add(newField);
        }

        return result;
    };

    add(field: Field)
    {
        field.struct = this;
        if (this._fields.has(field.name))
            throw new WTypeError(field.origin.originString, "Duplicate field name: " + field.name);
        this._fields.set(field.name, field);
    }

    get typeParameters() { return this._typeParameters; }

    get fieldNames() { return this._fields.keys(); }
    fieldByName(name: string) { return this._fields.get(name); }
    get fields() { return this._fields.values(); }
    get fieldMap() { return this._fields; }
    get isPrimitive()
    {
        let result = true;
        for (let field of this.fields)
            result = result && field.type.isPrimitive;
        return result;
    }
    toString()
    {
        return "struct " + this.name + "<" + this.typeParameters + "> { " + Array.from(this.fields).join("; ") + "; }";
    }
}

class Substitution extends Rewriter {
    _map: Map<Node1, Node1>;
    constructor(parameters: Node1[], argumentList: Node1[])
    {
        super();
        if (parameters.length != argumentList.length)
            throw new Error("Parameters and arguments are mismatched");
        this._map = new Map();
        for (let i = 0; i < parameters.length; ++i)
            this._map.set(parameters[i], argumentList[i]);
    }

    get map() { return this._map; }

    visitTypeRef(node: TypeRef)
    {
        let replacement = this._map.get(node.type);
        if (replacement) {
            if (node.typeArguments.length)
                throw new Error("Unexpected type arguments on type variable");
            let result = replacement.visit(new AutoWrapper());
            return result;
        }

        let result = super.visitTypeRef(node);
        return result;
    }

    visitVariableRef(node: VariableRef): Node1
    {
        let replacement = this._map.get(node.variable);
        if (replacement)
            return <Node1>replacement.visit(new AutoWrapper());

        return super.visitVariableRef(node);
    }
}

class InstantiationSubstitution extends Substitution {
    thisInstantiator: FuncInstantiator;
    constructor(thisInstantiator: FuncInstantiator, typeParameters: Node1[], typeArguments: Node1[]) {
        super(typeParameters, typeArguments);
        this.thisInstantiator = thisInstantiator;
    }
    visitCallExpression(node: CallExpression)
    {
        let result = <CallExpression>super.visitCallExpression(node);

        // We may have to re-resolve the function call, if it was a call to a protocol
        // signature.
        if (result.func instanceof ProtocolFuncDecl) {
            let overload = <OverLoad>resolveOverloadImpl(result.possibleOverloads, result.typeArguments, result.argumentTypes, result.returnType);
            if (!overload.func)
                throw new Error("Could not resolve protocol signature function call during instantiation: " + result.func + ((<OverLoadFailure>overload).failures.length ? "; tried:\n" + (<OverLoadFailure>overload).failures.join("\n") : ""));
            result.resolveToOverload(<OverLoadSuccess>overload);
        }

        if (result.func.isNative)
            (<CallExpression>result).nativeFuncInstance = <NativeFuncInstance>this.thisInstantiator.getUnique(result.func, result.actualTypeArguments);

        return result;
    }
}

class SwitchCase extends Node1 {
    // Null value means default.
    _value: Value;
    _body: Block;
    constructor(origin: Origin, value: Value, body: Block)
    {
        super();
        this._origin = origin;
        this._value = value;
        this._body = body;
    }

    get isDefault() { return !this._value; }
    get value() { return this._value; }
    get body() { return this._body; }

    toString()
    {
        let result = "";
        if (this.isDefault)
            result += "default";
        else
            result += "cast " + this.value;
        return result + ": " + this.body;
    }
}

class SwitchStatement extends Node1 {
    _switchCases: SwitchCase[];
    _value: Value;
    constructor(origin: Origin, value: Value)
    {
        super();
        this._origin = origin;
        this._value = value;
        this._switchCases = [];
    }

    get value() { return this._value; }

    add(switchCase: SwitchCase)
    {
        this._switchCases.push(switchCase);
    }

    get switchCases() { return this._switchCases; }

    toString()
    {
        let result = "switch (" + this.value + ") { ";
        if (this.switchCases.length)
            result += this.switchCases.join("; "); + "; ";
        return result + "}";
    }
}

function synthesizeEnumFunctions(program: Program)
{
    for (let type of program.types.values()) {
        if (!(type instanceof EnumType))
            continue;

        let nativeFunc: NativeFunc;
        let isCast = false;
        let shaderType;

        nativeFunc = new NativeFunc(
            type.origin, "operator==", new TypeRef(type.origin, "bool", []), [],
            [
                new FuncParameter(type.origin, null, new TypeRef(type.origin, type.name, [])),
                new FuncParameter(type.origin, null, new TypeRef(type.origin, type.name, []))
            ],
            isCast, shaderType);
        nativeFunc.implementation = ([left, right]) => EPtr.box((<EPtr>left).loadValue() == (<EPtr>right).loadValue());
        program.add(nativeFunc);

        nativeFunc = new NativeFunc(
            type.origin, "operator.value", <Type>type.baseType.visit(new Rewriter()), [],
            [new FuncParameter(type.origin, null, new TypeRef(type.origin, type.name, []))],
            isCast, shaderType);
        nativeFunc.implementation = ([value]) => <EPtr>value;
        program.add(nativeFunc);

        nativeFunc = new NativeFunc(
            type.origin, "operator cast", <Type>type.baseType.visit(new Rewriter()), [],
            [new FuncParameter(type.origin, null, new TypeRef(type.origin, type.name, []))],
            isCast, shaderType);
        nativeFunc.implementation = ([value]) => <EPtr>value;
        program.add(nativeFunc);

        nativeFunc = new NativeFunc(
            type.origin, "operator cast", new TypeRef(type.origin, type.name, []), [],
            [new FuncParameter(type.origin, null, <Type>type.baseType.visit(new Rewriter()))],
            isCast, shaderType);
        nativeFunc.implementation = ([value]) => <EPtr>value;
        program.add(nativeFunc);
    }
}

function createTypeParameters(type: Type): Node1[]
{
    let typeParameters = type.typeParameters;
    return typeParameters.map(
        (typeParameter: Node1): Node1 => { return <Node1>typeParameter.visit(new TypeParameterRewriter()); } );
}

function createTypeArguments(typeParameters: Node1[])
{
    return typeParameters.map(typeParameter => typeParameter.visit(new AutoWrapper()));
}

function setupImplementationData(nativeFunc: NativeFunc, implementation: (earguments: EPtr[], offset: int, structSize: int, fieldSize: int)=>EPtr, type: Type, field: Field)
{
    nativeFunc.instantiateImplementation = (substitution: Substitution) => {
        let newType = type.instantiate(nativeFunc.typeParameters.map(typeParameter => {
            let substitute = substitution.map.get(typeParameter);
            if (!substitute)
                throw new Error("Null substitute for type parameter " + typeParameter);
            return substitute;
        }));
        return <ImplementationDataType>{type: newType, fieldName: field.name, offset: -1, structSize: -1, fieldSize: -1};
    };
    nativeFunc.visitImplementationData = (implementationData: ImplementationDataType, visitor: Visitor) => {
        // Visiting the type first ensures that the struct layout builder figures out the field's
        // offset.
        implementationData.type.visit(visitor);
    };
    nativeFunc.didLayoutStructsInImplementationData = implementationData => {
        let structSize = implementationData.type.size;
        if (structSize == null)
            throw new Error("No struct size for " + nativeFunc);
        let field = (<StructType>implementationData.type).fieldByName(implementationData.fieldName);
        if (!field)
            throw new Error("Could not find field");
        let offset = field.offset;
        let fieldSize = field.type.size;
        if (fieldSize == null)
            throw new Error("No field size for " + nativeFunc);
        if (offset == null)
            throw new Error("No offset for " + nativeFunc);

        implementationData.offset = offset;
        implementationData.structSize = structSize;
        implementationData.fieldSize = fieldSize;
    };
    nativeFunc.implementation = (argumentList: EPtr[], node: Node1) => {
        let nativeFuncInstance = (<CallExpression>node).nativeFuncInstance;
        let implementationData = nativeFuncInstance.implementationData;
        return implementation(
            argumentList,
            implementationData.offset,
            implementationData.structSize,
            implementationData.fieldSize);
    };
}

function createFieldType(type: Type, typeParameters: Node1[], field: Field)
{
    return <Type>field.type.visit(new Substitution(type.typeParameters, typeParameters));
}

function createTypeRef(type: Type, typeParameters: Node1[])
{
    return TypeRef.instantiate(type, <Node1[]>createTypeArguments(typeParameters));
}

function synthesizeStructAccessors(program: Program)
{
    for (let type of program.types.values()) {
        if (!(type instanceof StructType))
            continue;

        for (let field of type.fields) {
            let isCast = false;
            let shaderType: string;
            let typeParameters: Node1[];
            let nativeFunc: NativeFunc;
            // The getter: operator.field
            typeParameters = createTypeParameters(type);
            nativeFunc = new NativeFunc(
                field.origin, "operator." + field.name, createFieldType(type, typeParameters, field), typeParameters,
                [new FuncParameter(field.origin, null, createTypeRef(type, typeParameters))], isCast, shaderType);
            setupImplementationData(nativeFunc, ([base], offset, structSize, fieldSize) => {
                let result = new EPtr(new EBuffer(fieldSize), 0);
                result.copyFrom(base.plus(offset), fieldSize);
                return result;
            }, type, field);
            program.add(nativeFunc);

            // The setter: operator.field=
            typeParameters = createTypeParameters(type);
            nativeFunc = new NativeFunc(
                field.origin, "operator." + field.name + "=", createTypeRef(type, typeParameters), typeParameters,
                [
                    new FuncParameter(field.origin, null, createTypeRef(type, typeParameters)),
                    new FuncParameter(field.origin, null, createFieldType(type, typeParameters, field))
                ],
                isCast, shaderType);
            setupImplementationData(nativeFunc, ([base, value], offset, structSize, fieldSize) => {
                let result = new EPtr(new EBuffer(structSize), 0);
                result.copyFrom(base, structSize);
                result.plus(offset).copyFrom(value, fieldSize);
                return result;
            }, type, field);
            program.add(nativeFunc);

            // The ander: operator&.field
            function setupAnder(addressSpace: string)
            {
                typeParameters = createTypeParameters(type);
                nativeFunc = new NativeFunc(
                    field.origin, "operator&." + field.name, new PtrType(field.origin, addressSpace, createFieldType(type, typeParameters, field)),
                    typeParameters,
                    [
                        new FuncParameter(
                            field.origin, null,
                            new PtrType(field.origin, addressSpace, createTypeRef(type, typeParameters)))
                    ],
                    isCast, shaderType);
                setupImplementationData(nativeFunc, ([base], offset, structSize, fieldSize) => {
                    base = <EPtr>base.loadValue();
                    if (!base)
                        throw new WTrapError(field.origin.originString, "Null dereference");
                    return EPtr.box(base.plus(offset));
                }, type, field);
                program.add(nativeFunc);
            }

            setupAnder("thread");
            setupAnder("threadgroup");
            setupAnder("device");
            setupAnder("constant");
        }
    }
}

class TrapStatement extends Node1 {
    constructor(origin: Origin)
    {
        super();
        this._origin = origin;
    }

    toString()
    {
        return "trap";
    }
};

class TypeDef extends Type {
    _name: string;
    _typeParameters: Node1[];
    constructor(origin: Origin, name: string, typeParameters: Node1[], type: Type)
    {
        super();
        this._origin = origin;
        this._name = name;
        this._typeParameters = typeParameters;
        this._type = type;
    }

    get typeParameters() { return this._typeParameters; }

    toString()
    {
        return "typedef " + this.name + "<" + this.typeParameters + "> = " + this.type;
    }
}


class TypeDefResolver extends Visitor {
    _visiting: VisitingSet;
    constructor()
    {
        super();
        this._visiting = new VisitingSet();
    }

    visitTypeRef(node: TypeRef)
    {
        this._visiting.doVisit(node, () => {
            for (let typeArgument of node.typeArguments)
                typeArgument.visit(this);
            if (node.type instanceof TypeDef) {
                let unificationContext = new UnificationContext(node.type.typeParameters);
                if (node.typeArguments.length != node.type.typeParameters.length)
                    throw new Error("argument/parameter mismatch (should have been caught earlier)");
                for (let i = 0; i < node.typeArguments.length; ++i)
                    node.typeArguments[i].unify(unificationContext, node.type.typeParameters[i]);
                let verificationResult = unificationContext.verify();
                if (!verificationResult.result)
                    throw new WTypeError(node.origin.originString, "Type reference to a type definition violates protocol constraints: " + verificationResult.reason);

                let newType = <Type>node.type.type.substituteToUnification(node.type.typeParameters, unificationContext);
                newType.visit(this);
                let array :Node1[] = new Array();
                node.setTypeAndArguments(newType, array);
            }
        });
    }
}

class TypeOrVariableRef extends Node1 {
    constructor(origin: Origin, name: string)
    {
        super();
        this._origin = origin;
        this._name = name;
    }

    toString()
    {
        return this.name;
    }
}

class TypeParameterRewriter {
    visitConstexprTypeParameter(node: ConstexprTypeParameter)
    {
        return new ConstexprTypeParameter(node.origin, node.name, <Type>node.type.visit(new Rewriter()));
    }

    visitTypeVariable(node: TypeVariable)
    {
        return new TypeVariable(node.origin, node.name, node.protocol);
    }
}

function TypeRefPopulate(buffer: EBuffer, offset: int) {
    if (!this.typeArguments.length)
        return this.type.populateDefaultValue(buffer, offset);
    throw new Error("Cannot get default value of a type instantiation");
}

class TypeRef extends Type {
    _name: string;
    _typeArguments: Node1[];
    constructor(origin: Origin, name: string, typeArguments: Node1[])
    {
        super();
        this._origin = origin;
        this._name = name;
        this.type = null;
        this._typeArguments = typeArguments;
        this.populateDefaultValue = TypeRefPopulate;
    }

    static wrap(type: Type)
    {
        if (type instanceof TypeRef && !type.typeArguments)
            return type;
        let name = type.name;
        let result = new TypeRef(type.origin, name, []);
        result.type = type;
        return result;
    }

    static instantiate(type: Type, typeArguments: Node1[])
    {
        let result = new TypeRef(type.origin, type.name, typeArguments);
        result.type = type;
        return result;
    }

    get typeArguments() { return this._typeArguments; }

    get unifyNode(): Node1
    {
        if (this.hasBecome) return this.target.unifyNode;
        if (!this.typeArguments.length)
            return this.type.unifyNode;
        return this;
    }
    get size()
    {
        if (!this.typeArguments.length)
            return this.type.size;
        throw new Error("Cannot get size of a type instantiation");
    }

    get isPrimitive()
    {
        if (!this.typeArguments.length)
            return this.type.isPrimitive;
        throw new Error("Cannot determine if an uninstantiated type is primitive: " + this);
    }

    setTypeAndArguments(type: Type, typeArguments: Node1[])
    {
        this._name = null;
        this.type = type;
        this._typeArguments = typeArguments;
    }

    unifyImpl(unificationContext: UnificationContext, other: TypeRef)
    {
        if (!(other instanceof TypeRef))
            return false;
        if (!this.type.unify(unificationContext, other.type))
            return false;
        if (this.typeArguments.length != other.typeArguments.length)
            return false;
        for (let i = 0; i < this.typeArguments.length; ++i) {
            if (!this.typeArguments[i].unify(unificationContext, other.typeArguments[i]))
                return false;
        }
        return true;
    }

    toString(): string
    {
        if (!this.name)
            return this.type.toString();
        if (!this.typeArguments.length)
            return this.name;
        return this.name + "<" + this.typeArguments + ">";
    }
}

class TypeVariable extends Type {
    _name: string;
    _protocol: ProtocolRef;
    constructor(origin: Origin, name: string, protocol: ProtocolRef)
    {
        super();
        this._origin = origin;
        this._name = name;
        this._protocol = protocol;
    }

    get protocol() { return this._protocol; }

    get isPrimitive()
    {
        return this._protocol && this._protocol.isPrimitive;
    }

    get isUnifiable() { return true; }

    inherits(protocol: ProtocolRef): VerityResultType
    {
        if (!protocol)
            return {result: true, reason: undefined};
        if (!this.protocol)
            return {result: false, reason: "Type variable " + this + " does not have a protocol"};
        return this.protocol.inherits(protocol);
    }

    typeVariableUnify(unificationContext: UnificationContext, other: TypeVariable): boolean
    {
        if (!(other instanceof Type))
            return false;

        return this._typeVariableUnifyImpl(unificationContext, other);
    }

    unifyImpl(unificationContext: UnificationContext, other: TypeVariable)
    {
        return this.typeVariableUnify(unificationContext, other);
    }

    verifyAsArgument(unificationContext: UnificationContext): VerityResultType
    {
        let realThis = unificationContext.find(this);

        // The thing we get unified with must be a type variable that accepts a broader set of
        // things than we do.
        if (!(realThis instanceof TypeVariable))
            return {result: false, reason: "Type variable argument " + this.toString() + " cannot be passed to non-type-variable parameter type " + realThis};

        if (!this.protocol) {
            if (realThis.protocol)
                return {result: false, reason: "Type variable without protocol " + this + " cannot be passed to parameter type variable with protocol " + realThis.protocol};
            return {result: true, reason: undefined};
        }

        let result = this.protocol.inherits(realThis.protocol);
        if (!result.result)
            return {result: false, reason: "Protocol " + this.protocol + " does not subsume protocol " + realThis.protocol + " (passing type " + this + " to type " + realThis + "): " + result.reason};
        return {result: true, reason: undefined};
    }

    verifyAsParameter(unificationContext: UnificationContext): VerityResultType
    {
        if (!this.protocol)
            return {result: true, reason: undefined};
        let realThis = <Type>unificationContext.find(this);
        let result = realThis.inherits(this.protocol);
        if (!result.result)
            return {result: false, reason: "Type " + realThis + " does not inherit protocol " + this.protocol + " (passing type " + realThis + " to type " + this.toString() + "): " + result.reason};
        return {result: true, reason: undefined};
    }

    toString()
    {
        return this.name + (this.protocol ? ":" + this.protocol.name : "");
    }
}

class TypeVariableTracker extends Visitor {
    _set: Set<Node1>;
    constructor()
    {
        super();
        this._set = new Set();
    }

    get set() { return this._set; }

    _consider(thing: Node1)
    {
        if (thing.isUnifiable)
            this._set.add(thing);
    }

    visitTypeRef(node: TypeRef)
    {
        if (node.typeArguments.length) {
            for (let typeArgument of node.typeArguments)
                typeArgument.visit(this);
            return;
        }
        this._consider(node.type);
    }

    visitVariableRef(node: VariableRef)
    {
        this._consider(node.variable);
    }
}

class TypedValue {
    _type: Type;
    _ePtr: EPtr;
    constructor(type: Type, ePtr: EPtr)
    {
        this._type = type;
        this._ePtr = ePtr;
    }

    get type() { return this._type; }
    get ePtr() { return this._ePtr; }

    static box(type: Type, value: EPtrBoxValue)
    {
        return new TypedValue(type, EPtr.box(value));
    }

    get value() { return this.ePtr.loadValue(); }

    toString()
    {
        return this.type + "(" + this.ePtr + ")";
    }
}

class UnificationContext {
    _typeParameters: Set<Node1>;
    _nextMap: Map<Node1, Node1>;
    _extraNodes: Set<Node1>;
    constructor(typeParameters?: Node1[])
    {
        this._typeParameters = new Set(typeParameters);
        this._nextMap = new Map();
        this._extraNodes = new Set();
    }

    union(a: Node1, b: Node1)
    {
        a = this.find(a);
        b = this.find(b);
        if (a == b)
            return;

        if (!a.isUnifiable) {
            [a, b] = [b, a];
            if (!a.isUnifiable)
                throw new Error("Cannot unify non-unifiable things " + a + " and " + b);
        }

        // Make sure that type parameters don't end up being roots.
        if (a.isUnifiable && b.isUnifiable && this._typeParameters.has(b))
            [a, b] = [b, a];

        this._nextMap.set(a, b);
    }

    find(node: Node1): Node1
    {
        let currentNode = node;
        let nextNode = this._nextMap.get(currentNode);
        if (!nextNode)
            return currentNode;
        for (;;) {
            currentNode = nextNode;
            nextNode = this._nextMap.get(currentNode);
            if (!nextNode)
                break;
        }
        this._nextMap.set(node, currentNode);
        return currentNode;
    }

    addExtraNode(node: Node1)
    {
        this._extraNodes.add(node);
    }

    get nodes()
    {
        let result: Set<Node1> = new Set();
        for (let [key, value] of this._nextMap) {
            result.add(key);
            result.add(value);
        }
        for (let node of this._extraNodes)
            result.add(node);
        return result;
    }

    typeParameters() { return this._typeParameters; }
    *typeArguments()
    {
        for (let typeArgument of this.nodes) {
            if (!typeArgument.isUnifiable)
                continue;
            if (this._typeParameters.has(typeArgument))
                continue;
            yield typeArgument;
        }
    }

    verify(): VerityResultType
    {
        // We do a two-phase pre-verification. This gives literals a chance to select a more specific type.
        let preparations: PrepareToVerityType[] = [];
        for (let node of this.nodes) {
            let preparation = <PrepareToVerityType>node.prepareToVerify(this);
            if (preparation)
                preparations.push(preparation);
        }
        for (let preparation of preparations) {
            let result = preparation();
            if (!result.result)
                return result;
        }

        for (let typeParameter of this._typeParameters) {
            let result = typeParameter.verifyAsParameter(this);
            if (!result.result)
                return result;
        }
        let numTypeVariableArguments = 0;
        let argumentSet = new Set();
        for (let typeArgument of this.typeArguments()) {
            let result = typeArgument.verifyAsArgument(this);
            if (!result.result)
                return result;
            if (typeArgument.isLiteral)
                continue;
            argumentSet.add(this.find(typeArgument));
            numTypeVariableArguments++;
        }
        if (argumentSet.size == numTypeVariableArguments)
            return {result: true, reason: undefined};
        return {result: false, reason: "Type variables used as arguments got unified with each other"};
    }

    get conversionCost()
    {
        let result = 0;
        for (let typeArgument of this.typeArguments())
            result += typeArgument.conversionCost(this);
        return result;
    }

    commit()
    {
        for (let typeArgument of this.typeArguments())
            typeArgument.commitUnification(this);
    }
}

class UnreachableCodeChecker extends Visitor {
    _returnChecker: ReturnChecker;
    constructor(program: Program)
    {
        super();
        this._returnChecker = new ReturnChecker(program);
    }

    visitBlock(node: Block)
    {
        super.visitBlock(node);
        for (let i = 0; i < node.statements.length - 1; ++i) {
            switch(node.statements[i].visit(this._returnChecker)) {
            case this._returnChecker.returnStyle.DefinitelyReturns:
            case this._returnChecker.returnStyle.DefinitelyDoesntReturn:
                throw new WTypeError(
                    node.statements[i + 1].origin.originString,
                    "Unreachable code");
            case this._returnChecker.returnStyle.HasntReturnedYet:
                continue;
            }
        }
    }
}

class VariableDecl extends Value {
    _initializer: Node1;
    _name: string;
    constructor(origin: Origin, name: string, type: Type, initializer: Expression)
    {
        super();
        this._origin = origin;
        this._name = name;
        this._type = type;
        this._initializer = initializer;
    }

    get initializer() { return this._initializer; }
    get varIsLValue() { return true; }

    toString()
    {
        return this.type + " " + this.name + (this.initializer ? " = " + this.initializer : "");
    }
}

class VariableRef extends Expression {
    variable: Value;
    _name: string;
    constructor(origin: Origin, name: string)
    {
        super(origin);
        this._name = name;
        this.variable = null;
        this._addressSpace = "thread";
    }

    get addressSpace() {
        if (this.hasBecome) return  this.target.addressSpace;
        return "thread";
    }

    static wrap(variable: Value)
    {
        let result = new VariableRef(variable.origin, variable.name);
        result.variable = variable;
        return result;
    }

    get isConstexpr() {
        if (this.hasBecome) return this.target.isConstexpr;
        return this.variable.isConstexpr;
    }
    get unifyNode() {
        if (this.hasBecome) return this.target.unifyNode;
        return this.variable.unifyNode;
    } // This only makes sense when this is a constexpr.
    get isLValue() {
        if (this.hasBecome) return this.target.isLValue;
        return this.variable.varIsLValue;
    }
    toString()
    {
        return this.name;
    }
}

class VisitingSet {
    _set: Set<Node1>;
    constructor(...items: Node1[])
    {
        this._set = new Set(items);
    }

    doVisit(item: Node1, callback: ()=>(Node1 | void))
    {
        if (this._set.has(item))
            throw new WTypeError(item.origin.originString, "Recursive " + (<typeof Node1>item.kind).name);
        this._set.add(item);
        try {
            return callback();
        } finally {
            this._set.delete(item);
        }
    }
}


class WSyntaxError extends Error {
    originString: string;
    syntaxErrorMessage: string;
    constructor(originString: string, message: string)
    {
        super("Syntax error at " + originString + ": " + message);
        this.originString = originString;
        this.syntaxErrorMessage = message;
    }
}

class WTrapError extends Error {
    originString: string;
    syntaxErrorMessage: string;
    constructor(originString: string, message: string)
    {
        super("Trap at " + originString + ": " + message);
        this.originString = originString;
        this.syntaxErrorMessage = message;
    }
}


class WTypeError extends Error {
    typeErrorMessage: string;
    originString: string;
    constructor(originString: string, message: string)
    {
        super("Type error at " + originString + ": " + message);
        this.originString = originString;
        this.typeErrorMessage = message;
    }
}

class WhileLoop extends Node1 {
    _conditional: CallExpression | FunctionLikeBlock;
    _body: Node1;
    constructor(origin: Origin, conditional: CallExpression, body: Node1)
    {
        super();
        this._origin = origin;
        this._conditional = conditional;
        this._body = body;
    }

    get conditional() { return this._conditional; }
    get body() { return this._body; }

    toString()
    {
        return "while (" + this.conditional + ") " + this.body;
    }
};

class WrapChecker extends Visitor {
    _startNode: Node1;
    constructor(node: Node1)
    {
        super();
        this._startNode = node;
    }

    visitVariableRef(node: VariableRef)
    {
    }

    visitTypeRef(node: TypeRef)
    {
    }

    _foundUnwrapped(node: Node1)
    {
        function originString(node: Node1)
        {
            let origin = node.origin;
            if (!origin)
                return "<null origin>";
            return origin.originString;
        }

        throw new Error("Found unwrapped " + node.constructor.name + " at " + originString(node) + ": " + node + "\nWhile visiting " + this._startNode.constructor.name + " at " + originString(this._startNode) + ": " + this._startNode);
    }

    visitConstexprTypeParameter(node: ConstexprTypeParameter)
    {
        this._foundUnwrapped(node);
    }

    visitFuncParameter(node: FuncParameter)
    {
        this._foundUnwrapped(node);
    }

    visitVariableDecl(node: VariableDecl)
    {
        this._foundUnwrapped(node);
    }

    visitStructType(node: StructType): undefined
    {
        this._foundUnwrapped(node);
        return undefined;
    }

    visitNativeType(node: NativeType)
    {
        this._foundUnwrapped(node);
    }

    visitTypeVariable(node: TypeVariable)
    {
        this._foundUnwrapped(node);
    }
}

function doPrep(code: string): Program
{
    return prepare("/internal/test", 0, code);
}

function doLex(code: string)
{
    let lexer = new Lexer("/internal/test", "native", 0, code);
    var result = [];
    for (;;) {
        let next = lexer.next();
        if (!next)
            return result;
        result.push(next);
    }
    return result;
}

function makeInt(program: Program, value: number)
{
    return TypedValue.box(program.intrinsics.int32, value);
}

function makeUint(program: Program, value: number)
{
    return TypedValue.box(program.intrinsics.uint32, value);
}

function makeUint8(program: Program, value: number)
{
    return TypedValue.box(program.intrinsics.uint8, value);
}

function makeBool(program: Program, value: boolean)
{
    return TypedValue.box(program.intrinsics.bool, value);
}

function makeFloat(program: Program, value: Node1)
{
    return TypedValue.box(program.intrinsics.float, value);
}

function makeDouble(program: Program, value: Node1)
{
    return TypedValue.box(program.intrinsics.double, value);
}

function makeEnum(program: Program, enumName: string, value: string)
{
    let enumType = <EnumType>program.types.get(enumName);
    if (!enumType)
        throw new Error("No type named " + enumName);
    let enumMember = enumType.memberByName(value);
    if (!enumMember)
        throw new Error("No member named " + enumMember + " in " + enumType);
    return TypedValue.box(enumType, (<EnumLiteral|IntLiteral>enumMember.value.unifyNode).valueForSelectedType);
}

function checkNumber(program: Program, result: TypedValue, expected: number)
{
    if (!(<GenericLiteralType | NativeType>result.type.unifyNode).isNumber)
        throw new Error("Wrong result type; result: " + result);
    if (result.value != expected)
        throw new Error("Wrong result: " + result.value + " (expected " + expected + ")");
}

function checkInt(program: Program, result: TypedValue, expected: number)
{
    if (!result.type.equals(program.intrinsics.int32))
        throw new Error("Wrong result type; result: " + result);
    checkNumber(program, result, expected);
}

function checkEnum(program: Program, result: TypedValue, expected: number)
{
    if (!(result.type.unifyNode instanceof EnumType))
        throw new Error("Wrong result type; result: " + result);
    if (result.value != expected)
        throw new Error("Wrong result: " + result.value + " (expected " + expected + ")");
}

function checkUint(program: Program, result: TypedValue, expected: number)
{
    if (!result.type.equals(program.intrinsics.uint32))
        throw new Error("Wrong result type: " + result.type);
    if (result.value != expected)
        throw new Error("Wrong result: " + result.value + " (expected " + expected + ")");
}

function checkUint8(program: Program, result: TypedValue, expected: number)
{
    if (!result.type.equals(program.intrinsics.uint8))
        throw new Error("Wrong result type: " + result.type);
    if (result.value != expected)
        throw new Error("Wrong result: " + result.value + " (expected " + expected + ")");
}

function checkBool(program: Program, result: TypedValue, expected: boolean)
{
    if (!result.type.equals(program.intrinsics.bool))
        throw new Error("Wrong result type: " + result.type);
    if (result.value != expected)
        throw new Error("Wrong result: " + result.value + " (expected " + expected + ")");
}

function checkFloat(program: Program, result: TypedValue, expected: number)
{
    if (!result.type.equals(program.intrinsics.float))
        throw new Error("Wrong result type: " + result.type);
    if (result.value != expected)
        throw new Error("Wrong result: " + result.value + " (expected " + expected + ")");
}

function checkDouble(program: Program, result: TypedValue, expected: number)
{
    if (!result.type.equals(program.intrinsics.double))
        throw new Error("Wrong result type: " + result.type);
    if (result.value != expected)
        throw new Error("Wrong result: " + result.value + " (expected " + expected + ")");
}

function checkLexerToken(result: LexerToken, expectedIndex: number, expectedKind: string, expectedText: string)
{
    if (result._index != expectedIndex)
        throw new Error("Wrong lexer index; result: " + result._index + " (expected " + expectedIndex + ")");
    if (result._kind != expectedKind)
        throw new Error("Wrong lexer kind; result: " + result._kind + " (expected " + expectedKind + ")");
    if (result._text != expectedText)
        throw new Error("Wrong lexer text; result: " + result._text + " (expected " + expectedText + ")");
}

function checkFail(callback: ()=>Program | TypedValue |void, predicate: (e: Error)=>boolean)
{
    try {
        callback();
        throw new Error("Did not throw exception");
    } catch (e) {
        if (predicate(e)) {
            return;
        }
        throw e;
    }
}

let okToTest = false;

/*
let tests = new Proxy({}, {
    set(target, property, value, receiver)
    {
        if (property in target)
            throw new Error("Trying to declare duplicate test: " + <string>property);
        target[property] = value;
        return true;
    }
});
*/
let tests: {[index:string]:()=>void} = {};

tests.literalBool = function() {
    let program = doPrep("bool foo() { return true; }");
    checkBool(program, callFunction(program, "foo", [], []), true);
}

tests.identityBool = function() {
    let program = doPrep("bool foo(bool x) { return x; }");
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, true)]), true);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, false)]), false);
}

tests.intSimpleMath = function() {
    let program = doPrep("int foo(int x, int y) { return x + y; }");
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 7), makeInt(program, 5)]), 12);
    program = doPrep("int foo(int x, int y) { return x - y; }");
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 7), makeInt(program, 5)]), 2);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 5), makeInt(program, 7)]), -2);
    program = doPrep("int foo(int x, int y) { return x * y; }");
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 7), makeInt(program, 5)]), 35);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 7), makeInt(program, -5)]), -35);
    program = doPrep("int foo(int x, int y) { return x / y; }");
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 7), makeInt(program, 2)]), 3);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 7), makeInt(program, -2)]), -3);
}

tests.uintSimpleMath = function() {
    let program = doPrep("uint foo(uint x, uint y) { return x + y; }");
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 7), makeUint(program, 5)]), 12);
    program = doPrep("uint foo(uint x, uint y) { return x - y; }");
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 7), makeUint(program, 5)]), 2);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 5), makeUint(program, 7)]), 4294967294);
    program = doPrep("uint foo(uint x, uint y) { return x * y; }");
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 7), makeUint(program, 5)]), 35);
    program = doPrep("uint foo(uint x, uint y) { return x / y; }");
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 7), makeUint(program, 2)]), 3);
}

tests.uint8SimpleMath = function() {
    let program = doPrep("uint8 foo(uint8 x, uint8 y) { return x + y; }");
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 7), makeUint8(program, 5)]), 12);
    program = doPrep("uint8 foo(uint8 x, uint8 y) { return x - y; }");
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 7), makeUint8(program, 5)]), 2);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 5), makeUint8(program, 7)]), 254);
    program = doPrep("uint8 foo(uint8 x, uint8 y) { return x * y; }");
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 7), makeUint8(program, 5)]), 35);
    program = doPrep("uint8 foo(uint8 x, uint8 y) { return x / y; }");
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 7), makeUint8(program, 2)]), 3);
}

tests.equality = function() {
    let program = doPrep("bool foo(uint x, uint y) { return x == y; }");
    checkBool(program, callFunction(program, "foo", [], [makeUint(program, 7), makeUint(program, 5)]), false);
    checkBool(program, callFunction(program, "foo", [], [makeUint(program, 7), makeUint(program, 7)]), true);
    program = doPrep("bool foo(uint8 x, uint8 y) { return x == y; }");
    checkBool(program, callFunction(program, "foo", [], [makeUint8(program, 7), makeUint8(program, 5)]), false);
    checkBool(program, callFunction(program, "foo", [], [makeUint8(program, 7), makeUint8(program, 7)]), true);
    program = doPrep("bool foo(int x, int y) { return x == y; }");
    checkBool(program, callFunction(program, "foo", [], [makeInt(program, 7), makeInt(program, 5)]), false);
    checkBool(program, callFunction(program, "foo", [], [makeInt(program, 7), makeInt(program, 7)]), true);
    program = doPrep("bool foo(bool x, bool y) { return x == y; }");
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, false), makeBool(program, true)]), false);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, true), makeBool(program, false)]), false);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, false), makeBool(program, false)]), true);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, true), makeBool(program, true)]), true);
}

tests.logicalNegation = function()
{
    let program = doPrep("bool foo(bool x) { return !x; }");
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, true)]), false);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, false)]), true);
}

tests.notEquality = function() {
    let program = doPrep("bool foo(uint x, uint y) { return x != y; }");
    checkBool(program, callFunction(program, "foo", [], [makeUint(program, 7), makeUint(program, 5)]), true);
    checkBool(program, callFunction(program, "foo", [], [makeUint(program, 7), makeUint(program, 7)]), false);
    program = doPrep("bool foo(uint8 x, uint8 y) { return x != y; }");
    checkBool(program, callFunction(program, "foo", [], [makeUint8(program, 7), makeUint8(program, 5)]), true);
    checkBool(program, callFunction(program, "foo", [], [makeUint8(program, 7), makeUint8(program, 7)]), false);
    program = doPrep("bool foo(int x, int y) { return x != y; }");
    checkBool(program, callFunction(program, "foo", [], [makeInt(program, 7), makeInt(program, 5)]), true);
    checkBool(program, callFunction(program, "foo", [], [makeInt(program, 7), makeInt(program, 7)]), false);
    program = doPrep("bool foo(bool x, bool y) { return x != y; }");
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, false), makeBool(program, true)]), true);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, true), makeBool(program, false)]), true);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, false), makeBool(program, false)]), false);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, true), makeBool(program, true)]), false);
}

tests.equalityTypeFailure = function()
{
    checkFail(
        () => doPrep("bool foo(int x, uint y) { return x == y; }"),
        (e: Error) => e instanceof WTypeError && e.message.indexOf("/internal/test:1") != -1);
}

tests.generalNegation = function()
{
    let program = doPrep("bool foo(int x) { return !x; }");
    checkBool(program, callFunction(program, "foo", [], [makeInt(program, 7)]), false);
    checkBool(program, callFunction(program, "foo", [], [makeInt(program, 0)]), true);
}

tests.add1 = function() {
    let program = doPrep("int foo(int x) { return x + 1; }");
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 42)]), 43);
}

tests.simpleGeneric = function() {
    let program = doPrep(`
        T id<T>(T x) { return x; }
        int foo(int x) { return id(x) + 1; }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 42)]), 43);
}

tests.nameResolutionFailure = function()
{
    checkFail(
        () => doPrep("int foo(int x) { return x + y; }"),
        (e) => e instanceof WTypeError && e.message.indexOf("/internal/test:1") != -1);
}

tests.simpleVariable = function()
{
    let program = doPrep(`
        int foo(int p)
        {
            int result = p;
            return result;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 42)]), 42);
}

tests.simpleAssignment = function()
{
    let program = doPrep(`
        int foo(int p)
        {
            int result;
            result = p;
            return result;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 42)]), 42);
}

tests.simpleDefault = function()
{
    let program = doPrep(`
        int foo()
        {
            int result;
            return result;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 0);
}

tests.simpleDereference = function()
{
    let program = doPrep(`
        int foo(device int* p)
        {
            return *p;
        }
    `);
    let buffer = new EBuffer(1);
    buffer.set(0, 13);
    checkInt(program, callFunction(program, "foo", [], [TypedValue.box(new PtrType(externalOrigin, "device", program.intrinsics.int32), new EPtr(buffer, 0))]), 13);
}

tests.dereferenceStore = function()
{
    let program = doPrep(`
        void foo(device int* p)
        {
            *p = 52;
        }
    `);
    let buffer = new EBuffer(1);
    buffer.set(0, 13);
    callFunction(program, "foo", [], [TypedValue.box(new PtrType(externalOrigin, "device", program.intrinsics.int32), new EPtr(buffer, 0))]);
    if (buffer.get(0) != 52)
        throw new Error("Expected buffer to contain 52 but it contains: " + buffer.get(0));
}

tests.simpleMakePtr = function()
{
    let program = doPrep(`
        thread int* foo()
        {
            int x = 42;
            return &x;
        }
    `);
    let result = callFunction(program, "foo", [], []);
    if (!result.type.isPtr)
        throw new Error("Return type is not a pointer: " + result.type);
    if (!(<ReferenceType>result.type).elementType.equals(program.intrinsics.int32))
        throw new Error("Return type is not a pointer to an int: " + result.type);
    if (!(result.value instanceof EPtr))
        throw new Error("Return value is not an EPtr: " + result.value);
    let value = result.value.loadValue();
    if (value != 42)
        throw new Error("Expected 42 but got: " + value);
}

tests.threadArrayLoad = function()
{
    let program = doPrep(`
        int foo(thread int[] array)
        {
            return array[0u];
        }
    `);
    let buffer = new EBuffer(1);
    buffer.set(0, 89);
    let result = callFunction(program, "foo", [], [TypedValue.box(new ArrayRefType(externalOrigin, "thread", program.intrinsics.int32), new EArrayRef(new EPtr(buffer, 0), 1))]);
    checkInt(program, result, 89);
}

tests.threadArrayLoadIntLiteral = function()
{
    let program = doPrep(`
        int foo(thread int[] array)
        {
            return array[0];
        }
    `);
    let buffer = new EBuffer(1);
    buffer.set(0, 89);
    let result = callFunction(program, "foo", [], [TypedValue.box(new ArrayRefType(externalOrigin, "thread", program.intrinsics.int32), new EArrayRef(new EPtr(buffer, 0), 1))]);
    checkInt(program, result, 89);
}

tests.deviceArrayLoad = function()
{
    let program = doPrep(`
        int foo(device int[] array)
        {
            return array[0u];
        }
    `);
    let buffer = new EBuffer(1);
    buffer.set(0, 89);
    let result = callFunction(program, "foo", [], [TypedValue.box(new ArrayRefType(externalOrigin, "device", program.intrinsics.int32), new EArrayRef(new EPtr(buffer, 0), 1))]);
    checkInt(program, result, 89);
}

tests.threadArrayStore = function()
{
    let program = doPrep(`
        void foo(thread int[] array, int value)
        {
            array[0u] = value;
        }
    `);
    let buffer = new EBuffer(1);
    buffer.set(0, 15);
    let arrayRef = TypedValue.box(
        new ArrayRefType(externalOrigin, "thread", program.intrinsics.int32),
        new EArrayRef(new EPtr(buffer, 0), 1));
    callFunction(program, "foo", [], [arrayRef, makeInt(program, 65)]);
    if (buffer.get(0) != 65)
        throw new Error("Bad value stored into buffer (expected 65): " + buffer.get(0));
    callFunction(program, "foo", [], [arrayRef, makeInt(program, -111)]);
    if (buffer.get(0) != -111)
        throw new Error("Bad value stored into buffer (expected -111): " + buffer.get(0));
}

tests.deviceArrayStore = function()
{
    let program = doPrep(`
        void foo(device int[] array, int value)
        {
            array[0u] = value;
        }
    `);
    let buffer = new EBuffer(1);
    buffer.set(0, 15);
    let arrayRef = TypedValue.box(
        new ArrayRefType(externalOrigin, "device", program.intrinsics.int32),
        new EArrayRef(new EPtr(buffer, 0), 1));
    callFunction(program, "foo", [], [arrayRef, makeInt(program, 65)]);
    if (buffer.get(0) != 65)
        throw new Error("Bad value stored into buffer (expected 65): " + buffer.get(0));
    callFunction(program, "foo", [], [arrayRef, makeInt(program, -111)]);
    if (buffer.get(0) != -111)
        throw new Error("Bad value stored into buffer (expected -111): " + buffer.get(0));
}

tests.deviceArrayStoreIntLiteral = function()
{
    let program = doPrep(`
        void foo(device int[] array, int value)
        {
            array[0] = value;
        }
    `);
    let buffer = new EBuffer(1);
    buffer.set(0, 15);
    let arrayRef = TypedValue.box(
        new ArrayRefType(externalOrigin, "device", program.intrinsics.int32),
        new EArrayRef(new EPtr(buffer, 0), 1));
    callFunction(program, "foo", [], [arrayRef, makeInt(program, 65)]);
    if (buffer.get(0) != 65)
        throw new Error("Bad value stored into buffer (expected 65): " + buffer.get(0));
    callFunction(program, "foo", [], [arrayRef, makeInt(program, -111)]);
    if (buffer.get(0) != -111)
        throw new Error("Bad value stored into buffer (expected -111): " + buffer.get(0));
}

tests.simpleProtocol = function()
{
    let program = doPrep(`
        protocol MyAddable {
            MyAddable operator+(MyAddable, MyAddable);
        }
        T add<T:MyAddable>(T a, T b)
        {
            return a + b;
        }
        int foo(int x)
        {
            return add(x, 73);
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 45)]), 45 + 73);
}

tests.typeMismatchReturn = function()
{
    checkFail(
        () => doPrep(`
            int foo()
            {
                return 0u;
            }
        `),
        (e) => e instanceof WTypeError);
}

tests.typeMismatchVariableDecl = function()
{
    checkFail(
        () => doPrep(`
            void foo(uint x)
            {
                int y = x;
            }
        `),
        (e) => e instanceof WTypeError);
}

tests.typeMismatchAssignment = function()
{
    checkFail(
        () => doPrep(`
            void foo(uint x)
            {
                int y;
                y = x;
            }
        `),
        (e) => e instanceof WTypeError);
}

tests.typeMismatchReturnParam = function()
{
    checkFail(
        () => doPrep(`
            int foo(uint x)
            {
                return x;
            }
        `),
        (e) => e instanceof WTypeError);
}

tests.badAdd = function()
{
    checkFail(
        () => doPrep(`
            void bar<T>(T) { }
            void foo(int x, uint y)
            {
                bar(x + y);
            }
        `),
        (e) => e instanceof WTypeError && e.message.indexOf("native int32 operator+<>(int32,int32)") != -1);
}

tests.lexerKeyword = function()
{
    let result = doLex("ident for while 123 123u { } {asd asd{ 1a3 1.2 + 3.4 + 1. + .2 1.2d 0.d .3d && ||");
    if (result.length != 25)
        throw new Error("Lexer emitted an incorrect number of tokens (expected 23): " + result.length);
    checkLexerToken(result[0],  0,  "identifier",    "ident");
    checkLexerToken(result[1],  6,  "keyword",       "for");
    checkLexerToken(result[2],  10, "keyword",       "while");
    checkLexerToken(result[3],  16, "intLiteral",    "123");
    checkLexerToken(result[4],  20, "uintLiteral",   "123u");
    checkLexerToken(result[5],  25, "punctuation",   "{");
    checkLexerToken(result[6],  27, "punctuation",   "}");
    checkLexerToken(result[7],  29, "punctuation",   "{");
    checkLexerToken(result[8],  30, "identifier",    "asd");
    checkLexerToken(result[9],  34, "identifier",    "asd");
    checkLexerToken(result[10], 37, "punctuation",   "{");
    checkLexerToken(result[11], 39, "intLiteral",    "1");
    checkLexerToken(result[12], 40, "identifier",    "a3");
    checkLexerToken(result[13], 43, "floatLiteral",  "1.2");
    checkLexerToken(result[14], 47, "punctuation",   "+");
    checkLexerToken(result[15], 49, "floatLiteral",  "3.4");
    checkLexerToken(result[16], 53, "punctuation",   "+");
    checkLexerToken(result[17], 55, "floatLiteral",  "1.");
    checkLexerToken(result[18], 58, "punctuation",   "+");
    checkLexerToken(result[19], 60, "floatLiteral",  ".2");
    checkLexerToken(result[20], 63, "floatLiteral",  "1.2d");
    checkLexerToken(result[21], 68, "floatLiteral",  "0.d");
    checkLexerToken(result[22], 72, "floatLiteral",  ".3d");
    checkLexerToken(result[23], 76, "punctuation",   "&&");
    checkLexerToken(result[24], 79, "punctuation",   "||");
}

tests.simpleNoReturn = function()
{
    checkFail(
        () => doPrep("int foo() { }"),
        (e) => e instanceof WTypeError);
}

tests.simpleUnreachableCode = function()
{
    checkFail(
        () => doPrep(`
            void foo()
            {
                return;
                int x;
            }
        `),
        (e) => e instanceof WTypeError);
}

tests.simpleStruct = function()
{
    let program = doPrep(`
        struct Foo {
            int x;
            int y;
        }
        Foo foo(Foo foo)
        {
            Foo result;
            result.x = foo.y;
            result.y = foo.x;
            return result;
        }
    `);
    let structType = program.types.get("Foo");
    if (!structType)
        throw new Error("Did not find Foo type");
    let buffer = new EBuffer(2);
    buffer.set(0, 62);
    buffer.set(1, 24);
    let result = callFunction(program, "foo", [], [new TypedValue(structType, new EPtr(buffer, 0))]);
    if (!result.type.equals(structType))
        throw new Error("Wrong result type: " + result.type);
    let x = result.ePtr.get(0);
    let y = result.ePtr.get(1);
    if (x != 24)
        throw new Error("Wrong result for x: " + x + " (y = " + y + ")");
    if (y != 62)
        throw new Error("Wrong result for y: " + y + " (x + " + x + ")");
}

tests.genericStructInstance = function()
{
    let program = doPrep(`
        struct Foo<T> {
            T x;
            T y;
        }
        Foo<int> foo(Foo<int> foo)
        {
            Foo<int> result;
            result.x = foo.y;
            result.y = foo.x;
            return result;
        }
    `);
    let structType = TypeRef.instantiate(program.types.get("Foo"), [program.intrinsics.int32]);
    let buffer = new EBuffer(2);
    buffer.set(0, 62);
    buffer.set(1, 24);
    let result = callFunction(program, "foo", [], [new TypedValue(structType, new EPtr(buffer, 0))]);
    let x = result.ePtr.get(0);
    let y = result.ePtr.get(1);
    if (x != 24)
        throw new Error("Wrong result for x: " + x + " (y = " + y + ")");
    if (y != 62)
        throw new Error("Wrong result for y: " + y + " (x + " + x + ")");
}

tests.doubleGenericCallsDoubleGeneric = function()
{
    doPrep(`
        void foo<T, U>(T, U) { }
        void bar<V, W>(V x, W y) { foo(x, y); }
    `);
}

tests.doubleGenericCallsSingleGeneric = function()
{
    checkFail(
        () => doPrep(`
            void foo<T>(T, T) { }
            void bar<V, W>(V x, W y) { foo(x, y); }
        `),
        (e) => e instanceof WTypeError);
}

tests.loadNull = function()
{
    checkFail(
        () => doPrep(`
            void sink<T>(T) { }
            void foo() { sink(*null); }
        `),
        (e) => e instanceof WTypeError && e.message.indexOf("Type passed to dereference is not a pointer: null") != -1);
}

tests.storeNull = function()
{
    checkFail(
        () => doPrep(`
            void foo() { *null = 42; }
        `),
        (e) => e instanceof WTypeError && e.message.indexOf("Type passed to dereference is not a pointer: null") != -1);
}

tests.returnNull = function()
{
    let program = doPrep(`
        thread int* foo() { return null; }
    `);
    let result = callFunction(program, "foo", [], []);
    if (!result.type.isPtr)
        throw new Error("Return type is not a pointer: " + result.type);
    if (!(<ReferenceType>result.type).elementType.equals(program.intrinsics.int32))
        throw new Error("Return type is not a pointer to an int: " + result.type);
    if (result.value != null)
        throw new Error("Return value is not null: " + result.value);
}

tests.dereferenceDefaultNull = function()
{
    let program = doPrep(`
        int foo()
        {
            thread int* p;
            return *p;
        }
    `);
    checkFail(
        () => callFunction(program, "foo", [], []),
        (e) => e instanceof WTrapError);
}

tests.defaultInitializedNull = function()
{
    let program = doPrep(`
        int foo()
        {
            thread int* p = null;;
            return *p;
        }
    `);
    checkFail(
        () => callFunction(program, "foo", [], []),
        (e) => e instanceof WTrapError);
}

tests.passNullToPtrMonomorphic = function()
{
    let program = doPrep(`
        int foo(thread int* ptr)
        {
            return *ptr;
        }
        int bar()
        {
            return foo(null);
        }
    `);
    checkFail(
        () => callFunction(program, "bar", [], []),
        (e) => e instanceof WTrapError);
}

tests.passNullToPtrPolymorphic = function()
{
    checkFail(
        () => doPrep(`
            T foo<T>(thread T* ptr)
            {
                return *ptr;
            }
            int bar()
            {
                return foo(null);
            }
        `),
        (e) => e instanceof WTypeError);
}

tests.passNullToPolymorphic = function()
{
    checkFail(
        () => doPrep(`
            T foo<T>(T ptr)
            {
                return ptr;
            }
            int bar()
            {
                return foo(null);
            }
        `),
        (e) => e instanceof WTypeError);
}

tests.loadNullArrayRef = function()
{
    checkFail(
        () => doPrep(`
            void sink<T>(T) { }
            void foo() { sink(null[0u]); }
        `),
        (e) => e instanceof WTypeError && e.message.indexOf("Cannot resolve access") != -1);
}

tests.storeNullArrayRef = function()
{
    checkFail(
        () => doPrep(`
            void foo() { null[0u] = 42; }
        `),
        (e) => e instanceof WTypeError && e.message.indexOf("Cannot resolve access") != -1);
}

tests.returnNullArrayRef = function()
{
    let program = doPrep(`
        thread int[] foo() { return null; }
    `);
    let result = callFunction(program, "foo", [], []);
    if (!result.type.isArrayRef)
        throw new Error("Return type is not an array reference: " + result.type);
    if (!(<ReferenceType>result.type).elementType.equals(program.intrinsics.int32))
        throw new Error("Return type is not an int array reference: " + result.type);
    if (result.value != null)
        throw new Error("Return value is not null: " + result.value);
}

tests.dereferenceDefaultNullArrayRef = function()
{
    let program = doPrep(`
        int foo()
        {
            thread int[] p;
            return p[0u];
        }
    `);
    checkFail(
        () => callFunction(program, "foo", [], []),
        (e) => e instanceof WTrapError);
}

tests.defaultInitializedNullArrayRef = function()
{
    let program = doPrep(`
        int foo()
        {
            thread int[] p = null;
            return p[0u];
        }
    `);
    checkFail(
        () => callFunction(program, "foo", [], []),
        (e) => e instanceof WTrapError);
}

tests.defaultInitializedNullArrayRefIntLiteral = function()
{
    let program = doPrep(`
        int foo()
        {
            thread int[] p = null;
            return p[0];
        }
    `);
    checkFail(
        () => callFunction(program, "foo", [], []),
        (e) => e instanceof WTrapError);
}

tests.passNullToPtrMonomorphicArrayRef = function()
{
    let program = doPrep(`
        int foo(thread int[] ptr)
        {
            return ptr[0u];
        }
        int bar()
        {
            return foo(null);
        }
    `);
    checkFail(
        () => callFunction(program, "bar", [], []),
        (e) => e instanceof WTrapError);
}

tests.passNullToPtrPolymorphicArrayRef = function()
{
    checkFail(
        () => doPrep(`
            T foo<T>(thread T[] ptr)
            {
                return ptr[0u];
            }
            int bar()
            {
                return foo(null);
            }
        `),
        (e) => e instanceof WTypeError);
}

tests.returnIntLiteralUint = function()
{
    let program = doPrep("uint foo() { return 42; }");
    checkNumber(program, callFunction(program, "foo", [], []), 42);
}

tests.returnIntLiteralDouble = function()
{
    let program = doPrep("double foo() { return 42; }");
    checkNumber(program, callFunction(program, "foo", [], []), 42);
}

tests.badIntLiteralForInt = function()
{
    checkFail(
        () => doPrep("void foo() { int x = 3000000000; }"),
        (e) => e instanceof WSyntaxError);
}

tests.badIntLiteralForUint = function()
{
    checkFail(
        () => doPrep("void foo() { uint x = 5000000000; }"),
        (e) => e instanceof WSyntaxError);
}

tests.badIntLiteralForDouble = function()
{
    checkFail(
        () => doPrep("void foo() { double x = 5000000000000000000000000000000000000; }"),
        (e) => e instanceof WSyntaxError);
}

tests.passNullAndNotNull = function()
{
    let program = doPrep(`
        T bar<T>(device T* p, device T*)
        {
            return *p;
        }
        int foo(device int* p)
        {
            return bar(p, null);
        }
    `);
    let buffer = new EBuffer(1);
    buffer.set(0, 13);
    checkInt(program, callFunction(program, "foo", [], [TypedValue.box(new PtrType(externalOrigin, "device", program.intrinsics.int32), new EPtr(buffer, 0))]), 13);
}

tests.passNullAndNotNullFullPoly = function()
{
    let program = doPrep(`
        T bar<T>(T p, T)
        {
            return p;
        }
        int foo(device int* p)
        {
            return *bar(p, null);
        }
    `);
    let buffer = new EBuffer(1);
    buffer.set(0, 13);
    checkInt(program, callFunction(program, "foo", [], [TypedValue.box(new PtrType(externalOrigin, "device", program.intrinsics.int32), new EPtr(buffer, 0))]), 13);
}

tests.passNullAndNotNullFullPolyReverse = function()
{
    let program = doPrep(`
        T bar<T>(T, T p)
        {
            return p;
        }
        int foo(device int* p)
        {
            return *bar(null, p);
        }
    `);
    let buffer = new EBuffer(1);
    buffer.set(0, 13);
    checkInt(program, callFunction(program, "foo", [], [TypedValue.box(new PtrType(externalOrigin, "device", program.intrinsics.int32), new EPtr(buffer, 0))]), 13);
}

tests.nullTypeVariableUnify = function()
{
    let left = new NullType(externalOrigin);
    let right = new TypeVariable(externalOrigin, "T", null);
    if (left.equals(right))
        throw new Error("Should not be equal but are: " + left + " and " + right);
    if (right.equals(left))
        throw new Error("Should not be equal but are: " + left + " and " + right);
    /*

    function everyOrder(array: string[][], callback: ())
    {
        function recurse(array, callback, order)
        {
            if (!array.length)
                return callback.call(null, order);

            for (let i = 0; i < array.length; ++i) {
                let nextArray = array.concat();
                nextArray.splice(i, 1);
                recurse(nextArray, callback, order.concat([array[i]]));
            }
        }

        recurse(array, callback, []);
    }

    function everyPair(things: string[])
    {
        let result = [];
        for (let i = 0; i < things.length; ++i) {
            for (let j = 0; j < things.length; ++j) {
                if (i != j)
                    result.push([things[i], things[j]]);
            }
        }
        return result;
    }

    everyOrder(
        everyPair(["nullType", "variableType", "ptrType"]),
        order => {
            let types = {};
            types.nullType = new NullType(externalOrigin);
            types.variableType = new TypeVariable(externalOrigin, "T", null);
            types.ptrType = new PtrType(externalOrigin, "constant", new NativeType(externalOrigin, "foo_t", []));
            let unificationContext = new UnificationContext([types.variableType]);
            for (let [leftName, rightName] of order) {
                let left = types[leftName];
                let right = types[rightName];
                let result = left.unify(unificationContext, right);
                if (!result)
                    throw new Error("In order " + order + " cannot unify " + left + " with " + right);
            }
            if (!unificationContext.verify().result)
                throw new Error("In order " + order.map(value => "(" + value + ")") + " cannot verify");
        });
        */
}

tests.doubleNot = function()
{
    let program = doPrep(`
        bool foo(bool x)
        {
            return !!x;
        }
    `);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, true)]), true);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, false)]), false);
}

tests.simpleRecursion = function()
{
    checkFail(
        () => doPrep(`
            void foo<T>(T x)
            {
                foo(&x);
            }
        `),
        (e) => e instanceof WTypeError);
}

tests.protocolMonoSigPolyDef = function()
{
    let program = doPrep(`
        struct IntAnd<T> {
            int first;
            T second;
        }
        IntAnd<T> intAnd<T>(int first, T second)
        {
            IntAnd<T> result;
            result.first = first;
            result.second = second;
            return result;
        }
        protocol IntAndable {
            IntAnd<int> intAnd(IntAndable, int);
        }
        int foo<T:IntAndable>(T first, int second)
        {
            IntAnd<int> result = intAnd(first, second);
            return result.first + result.second;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 54), makeInt(program, 12)]), 54 + 12);
}

tests.protocolPolySigPolyDef = function()
{
    let program = doPrep(`
        struct IntAnd<T> {
            int first;
            T second;
        }
        IntAnd<T> intAnd<T>(int first, T second)
        {
            IntAnd<T> result;
            result.first = first;
            result.second = second;
            return result;
        }
        protocol IntAndable {
            IntAnd<T> intAnd<T>(IntAndable, T);
        }
        int foo<T:IntAndable>(T first, int second)
        {
            IntAnd<int> result = intAnd(first, second);
            return result.first + result.second;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 54), makeInt(program, 12)]), 54 + 12);
}

tests.protocolDoublePolySigDoublePolyDef = function()
{
    let program = doPrep(`
        struct IntAnd<T, U> {
            int first;
            T second;
            U third;
        }
        IntAnd<T, U> intAnd<T, U>(int first, T second, U third)
        {
            IntAnd<T, U> result;
            result.first = first;
            result.second = second;
            result.third = third;
            return result;
        }
        protocol IntAndable {
            IntAnd<T, U> intAnd<T, U>(IntAndable, T, U);
        }
        int foo<T:IntAndable>(T first, int second, int third)
        {
            IntAnd<int, int> result = intAnd(first, second, third);
            return result.first + result.second + result.third;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 54), makeInt(program, 12), makeInt(program, 39)]), 54 + 12 + 39);
}

tests.protocolDoublePolySigDoublePolyDefExplicit = function()
{
    let program = doPrep(`
        struct IntAnd<T, U> {
            int first;
            T second;
            U third;
        }
        IntAnd<T, U> intAnd<T, U>(int first, T second, U third)
        {
            IntAnd<T, U> result;
            result.first = first;
            result.second = second;
            result.third = third;
            return result;
        }
        protocol IntAndable {
            IntAnd<T, U> intAnd<T, U>(IntAndable, T, U);
        }
        int foo<T:IntAndable>(T first, int second, int third)
        {
            IntAnd<int, int> result = intAnd<int, int>(first, second, third);
            return result.first + result.second + result.third;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 54), makeInt(program, 12), makeInt(program, 39)]), 54 + 12 + 39);
}

tests.variableShadowing = function()
{
    let program = doPrep(`
        int foo()
        {
            int y;
            int x = 7;
            {
                int x = 8;
                y = x;
            }
            return y;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 8);
    program = doPrep(`
        int foo()
        {
            int y;
            int x = 7;
            {
                int x = 8;
            }
            y = x;
            return y;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 7);
}

tests.ifStatement = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            int y = 6;
            if (x == 7) {
                y = 8;
            }
            return y;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 3)]), 6);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 4)]), 6);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 5)]), 6);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 6)]), 6);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 7)]), 8);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 8)]), 6);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 9)]), 6);
}

tests.ifElseStatement = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            int y = 6;
            if (x == 7) {
                y = 8;
            } else {
                y = 9;
            }
            return y;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 3)]), 9);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 4)]), 9);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 5)]), 9);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 6)]), 9);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 7)]), 8);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 8)]), 9);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 9)]), 9);
}

tests.ifElseIfStatement = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            int y = 6;
            if (x == 7) {
                y = 8;
            } else if (x == 8) {
                y = 9;
            }
            return y;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 3)]), 6);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 4)]), 6);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 5)]), 6);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 6)]), 6);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 7)]), 8);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 8)]), 9);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 9)]), 6);
}

tests.ifElseIfElseStatement = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            int y = 6;
            if (x == 7) {
                y = 8;
            } else if (x == 8) {
                y = 9;
            } else {
                y = 10;
            }
            return y;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 3)]), 10);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 4)]), 10);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 5)]), 10);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 6)]), 10);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 7)]), 8);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 8)]), 9);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 9)]), 10);
}

tests.returnIf = function()
{
    checkFail(
        () => doPrep(`
            int foo(int x)
            {
                int y = 6;
                if (x == 7) {
                    return y;
                }
            }
        `),
        (e) => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int foo(int x)
            {
                int y = 6;
                if (x == 7) {
                    return y;
                } else {
                    y = 8;
                }
            }
        `),
        (e) => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int foo(int x)
            {
                int y = 6;
                if (x == 7) {
                    y = 8;
                } else {
                    return y;
                }
            }
        `),
        (e) => e instanceof WTypeError);
    let program = doPrep(`
        int foo(int x)
        {
            int y = 6;
            if (x == 7) {
                return 8;
            } else {
                return 10;
            }
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 3)]), 10);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 4)]), 10);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 5)]), 10);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 6)]), 10);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 7)]), 8);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 8)]), 10);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 9)]), 10);
    checkFail(
        () => doPrep(`
            int foo(int x)
            {
                int y = 6;
                if (x == 7) {
                    return 8;
                } else if (x == 9) {
                    return 10;
                }
            }
        `),
        (e) => e instanceof WTypeError);
    program = doPrep(`
        int foo(int x)
        {
            int y = 6;
            if (x == 7) {
                return 8;
            } else {
                y = 9;
            }
            return y;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 3)]), 9);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 4)]), 9);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 5)]), 9);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 6)]), 9);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 7)]), 8);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 8)]), 9);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 9)]), 9);
    checkFail(
        () => doPrep(`
            int foo(int x)
            {
                int y = 6;
                if (x == 7) {
                    return 8;
                } else {
                    return 10;
                }
                return 11;
            }
        `),
        (e) => e instanceof WTypeError);
    program = doPrep(`
        int foo(int x)
        {
            int y = 6;
            if (x == 7)
                int y = 8;
            return y;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 7)]), 6);
}

tests.simpleWhile = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            while (x < 13)
                x = x * 2;
            return x;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 1)]), 16);
}

tests.protocolMonoPolySigDoublePolyDefExplicit = function()
{
    checkFail(
        () => {
            let program = doPrep(`
                struct IntAnd<T, U> {
                    int first;
                    T second;
                    U third;
                }
                IntAnd<T, U> intAnd<T, U>(int first, T second, U third)
                {
                    IntAnd<T, U> result;
                    result.first = first;
                    result.second = second;
                    result.third = third;
                    return result;
                }
                protocol IntAndable {
                    IntAnd<T, int> intAnd<T>(IntAndable, T, int);
                }
                int foo<T:IntAndable>(T first, int second, int third)
                {
                    IntAnd<int, int> result = intAnd<int>(first, second, third);
                    return result.first + result.second + result.third;
                }
            `);
            callFunction(program, "foo", [], [makeInt(program, 54), makeInt(program, 12), makeInt(program, 39)]);
        },
        (e) => e instanceof WTypeError);
}

tests.ambiguousOverloadSimple = function()
{
    checkFail(
        () => doPrep(`
            void foo<T>(int, T) { }
            void foo<T>(T, int) { }
            void bar(int a, int b) { foo(a, b); }
        `),
        (e) => e instanceof WTypeError);
}

tests.ambiguousOverloadOverlapping = function()
{
    checkFail(
        () => doPrep(`
            void foo<T>(int, T) { }
            void foo<T>(T, T) { }
            void bar(int a, int b) { foo(a, b); }
        `),
        (e) => e instanceof WTypeError);
}

tests.ambiguousOverloadTieBreak = function()
{
    doPrep(`
        void foo<T>(int, T) { }
        void foo<T>(T, T) { }
        void foo(int, int) { }
        void bar(int a, int b) { foo(a, b); }
    `);
}

tests.intOverloadResolution = function()
{
    let program = doPrep(`
        int foo(int) { return 1; }
        int foo(uint) { return 2; }
        int foo(double) { return 3; }
        int bar() { return foo(42); }
    `);
    checkInt(program, callFunction(program, "bar", [], []), 1);
}

tests.intOverloadResolutionReverseOrder = function()
{
    let program = doPrep(`
        int foo(double) { return 3; }
        int foo(uint) { return 2; }
        int foo(int) { return 1; }
        int bar() { return foo(42); }
    `);
    checkInt(program, callFunction(program, "bar", [], []), 1);
}

tests.intOverloadResolutionGeneric = function()
{
    let program = doPrep(`
        int foo(int) { return 1; }
        int foo<T>(T) { return 2; }
        int bar() { return foo(42); }
    `);
    checkInt(program, callFunction(program, "bar", [], []), 1);
}

tests.intLiteralGeneric = function()
{
    let program = doPrep(`
        int foo<T>(T x) { return 3478; }
        int bar() { return foo(42); }
    `);
    checkInt(program, callFunction(program, "bar", [], []), 3478);
}

tests.intLiteralGenericWithProtocols = function()
{
    let program = doPrep(`
        protocol MyConvertibleToInt {
            operator int(MyConvertibleToInt);
        }
        int foo<T:MyConvertibleToInt>(T x) { return int(x); }
        int bar() { return foo(42); }
    `);
    checkInt(program, callFunction(program, "bar", [], []), 42);
}

tests.uintLiteralGeneric = function()
{
    let program = doPrep(`
        int foo<T>(T x) { return 3478; }
        int bar() { return foo(42u); }
    `);
    checkInt(program, callFunction(program, "bar", [], []), 3478);
}

tests.uintLiteralGenericWithProtocols = function()
{
    let program = doPrep(`
        protocol MyConvertibleToUint {
            operator uint(MyConvertibleToUint);
        }
        uint foo<T:MyConvertibleToUint>(T x) { return uint(x); }
        uint bar() { return foo(42u); }
    `);
    checkUint(program, callFunction(program, "bar", [], []), 42);
}

tests.intLiteralGenericSpecific = function()
{
    let program = doPrep(`
        T foo<T>(T x) { return x; }
        int bar() { return foo(int(42)); }
    `);
    checkInt(program, callFunction(program, "bar", [], []), 42);
}

tests.simpleConstexpr = function()
{
    let program = doPrep(`
        int foo<int a>(int b)
        {
            return a + b;
        }
        int bar(int b)
        {
            return foo<42>(b);
        }
    `);
    checkInt(program, callFunction(program, "bar", [], [makeInt(program, 58)]), 58 + 42);
}

tests.break = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            while (true) {
                x = x * 2;
                if (x >= 7)
                    break;
            }
            return x;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 1)]), 8);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 10)]), 20);
    program = doPrep(`
        int foo(int x)
        {
            while (true) {
                while (true) {
                    x = x * 2;
                    if (x >= 7)
                        break;
                }
                x = x - 1;
                break;
            }
            return x;

        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 1)]), 7);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 10)]), 19);
    checkFail(
        () => doPrep(`
            int foo(int x)
            {
                while (true) {
                    {
                        break;
                    }
                    x = x + 1;
                }
                return x;
            }
        `),
        (e) => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int foo(int x)
            {
                break;
                return x;
            }
        `),
        (e) => e instanceof WTypeError);
    program = doPrep(`
            int foo(int x)
            {
                while (true) {
                    if (x == 7) {
                        break;
                    }
                    x = x + 1;
                }
                return x;
            }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 1)]), 7);
    program = doPrep(`
            int foo(int x)
            {
                while (true) {
                    break;
                }
                return x;
            }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 1)]), 1);
    program = doPrep(`
            int foo()
            {
                while (true) {
                    return 7;
                }
            }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 7);
    checkFail(
        () => doPrep(`
            int foo(int x)
            {
                while(true) {
                    break;
                    return 7;
                }
            }
        `),
        (e) => e instanceof WTypeError);
}

tests.continue = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            while (x < 10) {
                if (x == 8) {
                    x = x + 1;
                    continue;
                }
                x = x * 2;
            }
            return x;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 1)]), 18);
    checkFail(
        () => doPrep(`
            int foo(int x)
            {
                continue;
                return x;

            }
        `),
        (e) => e instanceof WTypeError);
}

tests.doWhile = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            int y = 7;
            do {
                y = 8;
                break;
            } while (x < 10);
            return y;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 1)]), 8);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 11)]), 8);
    program = doPrep(`
        int foo(int x)
        {
            int y = 7;
            do {
                y = 8;
                break;
            } while (y == 7);
            return y;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 1)]), 8);
    program = doPrep(`
        int foo(int x)
        {
            int sum = 0;
            do {
                if (x == 11) {
                    x = 15;
                    continue;
                }
                sum = sum + x;
                x = x + 1;
            } while (x < 13);
            return sum;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 9)]), 19);
}

tests.forLoop = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            int sum = 0;
            int i;
            for (i = 0; i < x; i = i + 1) {
                sum = sum + i;
            }
            return sum;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 3)]), 3);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 4)]), 6);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 5)]), 10);
    program = doPrep(`
        int foo(int x)
        {
            int sum = 0;
            for (int i = 0; i < x; i = i + 1) {
                sum = sum + i;
            }
            return sum;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 3)]), 3);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 4)]), 6);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 5)]), 10);
    program = doPrep(`
        int foo(int x)
        {
            int sum = 0;
            int i = 100;
            for (int i = 0; i < x; i = i + 1) {
                sum = sum + i;
            }
            return sum;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 3)]), 3);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 4)]), 6);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 5)]), 10);
    program = doPrep(`
        int foo(int x)
        {
            int sum = 0;
            for (int i = 0; i < x; i = i + 1) {
                if (i == 4)
                    continue;
                sum = sum + i;
            }
            return sum;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 3)]), 3);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 4)]), 6);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 5)]), 6);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 6)]), 11);
    program = doPrep(`
        int foo(int x)
        {
            int sum = 0;
            for (int i = 0; i < x; i = i + 1) {
                if (i == 5)
                    break;
                sum = sum + i;
            }
            return sum;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 3)]), 3);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 4)]), 6);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 5)]), 10);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 6)]), 10);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 7)]), 10);
    program = doPrep(`
        int foo(int x)
        {
            int sum = 0;
            for (int i = 0; ; i = i + 1) {
                if (i >= x)
                    break;
                sum = sum + i;
            }
            return sum;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 3)]), 3);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 4)]), 6);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 5)]), 10);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 6)]), 15);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 7)]), 21);
    program = doPrep(`
        int foo(int x)
        {
            int sum = 0;
            int i = 0;
            for ( ; ; i = i + 1) {
                if (i >= x)
                    break;
                sum = sum + i;
            }
            return sum;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 3)]), 3);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 4)]), 6);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 5)]), 10);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 6)]), 15);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 7)]), 21);
    program = doPrep(`
        int foo(int x)
        {
            int sum = 0;
            int i = 0;
            for ( ; ; ) {
                if (i >= x)
                    break;
                sum = sum + i;
                i = i + 1;
            }
            return sum;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 3)]), 3);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 4)]), 6);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 5)]), 10);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 6)]), 15);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 7)]), 21);
    checkFail(
        () => doPrep(`
            void foo(int x)
            {
                for (int i = 0; ; i = i + 1) {
                    break;
                    x = i;
                }
            }
        `),
        (e) => e instanceof WTypeError);
    program = doPrep(`
        int foo(int x)
        {
            for ( ; ; ) {
                return 7;
            }
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 3)]), 7);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 4)]), 7);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 5)]), 7);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 6)]), 7);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 7)]), 7);
    checkFail(
        () => doPrep(`
            int foo(int x)
            {
                for ( ; x < 10; ) {
                    return 7;
                }
            }
        `),
        (e) => e instanceof WTypeError);
    program = doPrep(`
        int foo(int x)
        {
            for ( ; true; ) {
                return 7;
            }
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 3)]), 7);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 4)]), 7);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 5)]), 7);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 6)]), 7);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 7)]), 7);
}

tests.chainConstexpr = function()
{
    let program = doPrep(`
        int foo<int a>(int b)
        {
            return a + b;
        }
        int bar<int a>(int b)
        {
            return foo<a>(b);
        }
        int baz(int b)
        {
            return bar<42>(b);
        }
    `);
    checkInt(program, callFunction(program, "baz", [], [makeInt(program, 58)]), 58 + 42);
}

tests.chainGeneric = function()
{
    let program = doPrep(`
        T foo<T>(T x)
        {
            return x;
        }
        T bar<T>(thread T* ptr)
        {
            return *foo(ptr);
        }
        int baz(int x)
        {
            return bar(&x);
        }
    `);
    checkInt(program, callFunction(program, "baz", [], [makeInt(program, 37)]), 37);
}

tests.chainStruct = function()
{
    let program = doPrep(`
        struct Foo<T> {
            T f;
        }
        struct Bar<T> {
            Foo<thread T*> f;
        }
        int foo(thread Bar<int>* x)
        {
            return *x->f.f;
        }
        int bar(int a)
        {
            Bar<int> x;
            x.f.f = &a;
            return foo(&x);
        }
    `);
    checkInt(program, callFunction(program, "bar", [], [makeInt(program, 4657)]), 4657);
}

tests.chainStructNewlyValid = function()
{
    let program = doPrep(`
        struct Foo<T> {
            T f;
        }
        struct Bar<T> {
            Foo<device T*> f;
        }
        int foo(thread Bar<int>* x)
        {
            return *x->f.f;
        }
        int bar(device int* a)
        {
            Bar<int> x;
            x.f.f = a;
            return foo(&x);
        }
    `);
    let buffer = new EBuffer(1);
    buffer.set(0, 78453);
    checkInt(program, callFunction(program, "bar", [], [TypedValue.box(new PtrType(externalOrigin, "device", program.intrinsics.int32), new EPtr(buffer, 0))]), 78453);
}

tests.chainStructDevice = function()
{
    let program = doPrep(`
        struct Foo<T> {
            T f;
        }
        struct Bar<T> {
            Foo<device T*> f;
        }
        int foo(thread Bar<int>* x)
        {
            return *x->f.f;
        }
        int bar(device int* a)
        {
            Bar<int> x;
            x.f.f = a;
            return foo(&x);
        }
    `);
    let buffer = new EBuffer(1);
    buffer.set(0, 79201);
    checkInt(program, callFunction(program, "bar", [], [TypedValue.box(new PtrType(externalOrigin, "device", program.intrinsics.int32), new EPtr(buffer, 0))]), 79201);
}

tests.paramChainStructDevice = function()
{
    let program = doPrep(`
        struct Foo<T> {
            T f;
        }
        struct Bar<T> {
            Foo<T> f;
        }
        int foo(thread Bar<device int*>* x)
        {
            return *x->f.f;
        }
        int bar(device int* a)
        {
            Bar<device int*> x;
            x.f.f = a;
            return foo(&x);
        }
    `);
    let buffer = new EBuffer(1);
    buffer.set(0, 79201);
    checkInt(program, callFunction(program, "bar", [], [TypedValue.box(new PtrType(externalOrigin, "device", program.intrinsics.int32), new EPtr(buffer, 0))]), 79201);
}

tests.simpleProtocolExtends = function()
{
    let program = doPrep(`
        protocol Foo {
            void foo(thread Foo*);
        }
        protocol Bar : Foo {
            void bar(thread Bar*);
        }
        void fuzz<T:Foo>(thread T* p)
        {
            foo(p);
        }
        void buzz<T:Bar>(thread T* p)
        {
            fuzz(p);
            bar(p);
        }
        void foo(thread int* p)
        {
            *p = *p + 743;
        }
        void bar(thread int* p)
        {
            *p = *p + 91;
        }
        int thingy(int a)
        {
            buzz(&a);
            return a;
        }
    `);
    checkInt(program, callFunction(program, "thingy", [], [makeInt(program, 642)]), 642 + 743 + 91);
}

tests.protocolExtendsTwo = function()
{
    let program = doPrep(`
        protocol Foo {
            void foo(thread Foo*);
        }
        protocol Bar {
            void bar(thread Bar*);
        }
        protocol Baz : Foo, Bar {
            void baz(thread Baz*);
        }
        void fuzz<T:Foo>(thread T* p)
        {
            foo(p);
        }
        void buzz<T:Bar>(thread T* p)
        {
            bar(p);
        }
        void xuzz<T:Baz>(thread T* p)
        {
            fuzz(p);
            buzz(p);
            baz(p);
        }
        void foo(thread int* p)
        {
            *p = *p + 743;
        }
        void bar(thread int* p)
        {
            *p = *p + 91;
        }
        void baz(thread int* p)
        {
            *p = *p + 39;
        }
        int thingy(int a)
        {
            xuzz(&a);
            return a;
        }
    `);
    checkInt(program, callFunction(program, "thingy", [], [makeInt(program, 642)]), 642 + 743 + 91 + 39);
}

tests.prefixPlusPlus = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            ++x;
            return x;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 64)]), 65);
}

tests.prefixPlusPlusResult = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            return ++x;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 64)]), 65);
}

tests.postfixPlusPlus = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            x++;
            return x;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 64)]), 65);
}

tests.postfixPlusPlusResult = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            return x++;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 64)]), 64);
}

tests.prefixMinusMinus = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            --x;
            return x;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 64)]), 63);
}

tests.prefixMinusMinusResult = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            return --x;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 64)]), 63);
}

tests.postfixMinusMinus = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            x--;
            return x;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 64)]), 63);
}

tests.postfixMinusMinusResult = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            return x--;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 64)]), 64);
}

tests.plusEquals = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            x += 42;
            return x;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 385)]), 385 + 42);
}

tests.plusEqualsResult = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            return x += 42;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 385)]), 385 + 42);
}

tests.minusEquals = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            x -= 42;
            return x;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 385)]), 385 - 42);
}

tests.minusEqualsResult = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            return x -= 42;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 385)]), 385 - 42);
}

tests.timesEquals = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            x *= 42;
            return x;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 385)]), 385 * 42);
}

tests.timesEqualsResult = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            return x *= 42;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 385)]), 385 * 42);
}

tests.divideEquals = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            x /= 42;
            return x;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 385)]), (385 / 42) | 0);
}

tests.divideEqualsResult = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            return x /= 42;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 385)]), (385 / 42) | 0);
}

tests.twoIntLiterals = function()
{
    let program = doPrep(`
        bool foo()
        {
            return 42 == 42;
        }
    `);
    checkBool(program, callFunction(program, "foo", [], []), true);
}

tests.unifyDifferentLiterals = function()
{
    checkFail(
        () => doPrep(`
            void bar<T>(T, T)
            {
            }
            void foo()
            {
                bar(42, 42u);
            }
        `),
        (e) => e instanceof WTypeError);
}

tests.unifyDifferentLiteralsBackwards = function()
{
    checkFail(
        () => doPrep(`
            void bar<T>(T, T)
            {
            }
            void foo()
            {
                bar(42u, 42);
            }
        `),
        (e) => e instanceof WTypeError);
}

tests.unifyVeryDifferentLiterals = function()
{
    checkFail(
        () => doPrep(`
            void bar<T>(T, T)
            {
            }
            void foo()
            {
                bar(42, null);
            }
        `),
        (e) => e instanceof WTypeError);
}

tests.unifyVeryDifferentLiteralsBackwards = function()
{
    checkFail(
        () => doPrep(`
            void bar<T>(T, T)
            {
            }
            void foo()
            {
                bar(null, 42);
            }
        `),
        (e) => e instanceof WTypeError);
}

tests.assignUintToInt = function()
{
    checkFail(
        () => doPrep(`
            void foo()
            {
                int x = 42u;
            }
        `),
        (e) => e instanceof WTypeError && e.message.indexOf("Type mismatch in variable initialization") != -1);
}

tests.buildArrayThenSumIt = function()
{
    let program = doPrep(`
        int foo()
        {
            int[42] array;
            for (uint i = 0; i < 42; i = i + 1)
                array[i] = int(i + 5);
            int result;
            for (uint i = 0; i < 42; i = i + 1)
                result = result + array[i];
            return result;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 42 * 5 + 42 * 41 / 2);
}

tests.buildArrayThenSumItUsingArrayReference = function()
{
    let program = doPrep(`
        int bar(thread int[] array)
        {
            for (uint i = 0; i < 42; i = i + 1)
                array[i] = int(i + 5);
            int result;
            for (uint i = 0; i < 42; i = i + 1)
                result = result + array[i];
            return result;
        }
        int foo()
        {
            int[42] array;
            return bar(@array);
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 42 * 5 + 42 * 41 / 2);
}

tests.overrideSubscriptStruct = function()
{
    let program = doPrep(`
        struct Foo {
            int x;
            int y;
        }
        thread int* operator&[](thread Foo* foo, uint index)
        {
            if (index == 0)
                return &foo->x;
            if (index == 1)
                return &foo->y;
            return null;
        }
        int foo()
        {
            Foo foo;
            foo.x = 498;
            foo.y = 19;
            return foo[0] + foo[1] * 3;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 498 + 19 * 3);
}

tests.overrideSubscriptStructAndDoStores = function()
{
    let program = doPrep(`
        struct Foo {
            int x;
            int y;
        }
        thread int* operator&[](thread Foo* foo, uint index)
        {
            if (index == 0)
                return &foo->x;
            if (index == 1)
                return &foo->y;
            return null;
        }
        int foo()
        {
            Foo foo;
            foo[0] = 498;
            foo[1] = 19;
            return foo.x + foo.y;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 498 + 19);
}

tests.overrideSubscriptStructAndUsePointers = function()
{
    let program = doPrep(`
        struct Foo {
            int x;
            int y;
        }
        thread int* operator&[](thread Foo* foo, uint index)
        {
            if (index == 0)
                return &foo->x;
            if (index == 1)
                return &foo->y;
            return null;
        }
        int bar(thread Foo* foo)
        {
            return (*foo)[0] + (*foo)[1];
        }
        int foo()
        {
            Foo foo;
            foo.x = 498;
            foo.y = 19;
            return bar(&foo);
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 498 + 19);
}

tests.overrideSubscriptStructAndUsePointersIncorrectly = function()
{
    checkFail(
        () => doPrep(`
            struct Foo {
                int x;
                int y;
            }
            thread int* operator&[](thread Foo* foo, uint index)
            {
                if (index == 0)
                    return &foo->x;
                if (index == 1)
                    return &foo->y;
                return null;
            }
            int bar(thread Foo* foo)
            {
                return foo[0] + foo[1];
            }
            int foo()
            {
                Foo foo;
                foo.x = 498;
                foo.y = 19;
                return bar(&foo);
            }
        `),
        (e) => e instanceof WTypeError);
}

tests.makeArrayRefFromLocal = function()
{
    let program = doPrep(`
        int bar(thread int[] ref)
        {
            return ref[0];
        }
        int foo()
        {
            int x = 48;
            return bar(@x);
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 48);
}

tests.makeArrayRefFromPointer = function()
{
    let program = doPrep(`
        int bar(thread int[] ref)
        {
            return ref[0];
        }
        int baz(thread int* ptr)
        {
            return bar(@ptr);
        }
        int foo()
        {
            int x = 48;
            return baz(&x);
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 48);
}

tests.makeArrayRefFromArrayRef = function()
{
    checkFail(
        () => doPrep(`
            int bar(thread int[] ref)
            {
                return ref[0];
            }
            int baz(thread int[] ptr)
            {
                return bar(@ptr);
            }
            int foo()
            {
                int x = 48;
                return baz(@x);
            }
        `),
        (e) => e instanceof WTypeError);
}

tests.simpleLength = function()
{
    let program = doPrep(`
        uint foo()
        {
            double[754] array;
            return (@array).length;
        }
    `);
    checkUint(program, callFunction(program, "foo", [], []), 754);
}

tests.nonArrayRefArrayLengthSucceed = function()
{
    let program = doPrep(`
        uint foo()
        {
            double[754] array;
            return array.length;
        }
    `);
    checkUint(program, callFunction(program, "foo", [], []), 754);
}

tests.nonArrayRefArrayLengthFail = function()
{
    checkFail(
        () => doPrep(`
            thread uint* lengthPtr()
            {
                int[42] array;
                return &(array.length);
            }
        `),
        e => e instanceof WTypeError);
}

tests.constexprIsNotLValuePtr = function()
{
    checkFail(
        () => doPrep(`
            thread int* foo<int x>()
            {
                return &x;
            }
        `),
        e => e instanceof WTypeError);
}

tests.constexprIsNotLValueAssign = function()
{
    checkFail(
        () => doPrep(`
            void foo<int x>()
            {
                x = 42;
            }
        `),
        e => e instanceof WTypeError);
}

tests.constexprIsNotLValueRMW = function()
{
    checkFail(
        () => doPrep(`
            void foo<int x>()
            {
                x += 42;
            }
        `),
        e => e instanceof WTypeError);
}

tests.assignLength = function()
{
    checkFail(
        () => doPrep(`
            void foo()
            {
                double[754] array;
                (@array).length = 42;
            }
        `),
        (e) => e instanceof WTypeError && e.message.indexOf("Have neither ander nor setter") != -1);
}

tests.assignLengthHelper = function()
{
    checkFail(
        () => doPrep(`
            void bar(thread double[] array)
            {
                array.length = 42;
            }
            void foo()
            {
                double[754] array;
                bar(@array);
            }
        `),
        (e) => e instanceof WTypeError && e.message.indexOf("Have neither ander nor setter") != -1);
}

tests.simpleGetter = function()
{
    let program = doPrep(`
        struct Foo {
            int x;
        }
        int operator.y(Foo foo)
        {
            return foo.x;
        }
        int foo()
        {
            Foo foo;
            foo.x = 7804;
            return foo.y;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 7804);
}

tests.simpleSetter = function()
{
    let program = doPrep(`
        struct Foo {
            int x;
        }
        int operator.y(Foo foo)
        {
            return foo.x;
        }
        Foo operator.y=(Foo foo, int value)
        {
            foo.x = value;
            return foo;
        }
        int foo()
        {
            Foo foo;
            foo.y = 7804;
            return foo.x;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 7804);
}

tests.genericAccessors = function()
{
    let program = doPrep(`
        struct Foo<T> {
            T x;
            T[3] y;
        }
        struct Bar<T> {
            T x;
            T y;
        }
        Bar<T> operator.z<T>(Foo<T> foo)
        {
            Bar<T> result;
            result.x = foo.x;
            result.y = foo.y[1];
            return result;
        }
        Foo<T> operator.z=<T>(Foo<T> foo, Bar<T> bar)
        {
            foo.x = bar.x;
            foo.y[1] = bar.y;
            return foo;
        }
        T operator.sum<T:Addable>(Foo<T> foo)
        {
            return foo.x + foo.y[0] + foo.y[1] + foo.y[2];
        }
        T operator.sum<T:Addable>(Bar<T> bar)
        {
            return bar.x + bar.y;
        }
        operator<T> Bar<T>(T x, T y)
        {
            Bar<T> result;
            result.x = x;
            result.y = y;
            return result;
        }
        void setup(thread Foo<int>* foo)
        {
            foo->x = 1;
            foo->y[0] = 2;
            foo->y[1] = 3;
            foo->y[2] = 4;
        }
        int testSuperBasic()
        {
            Foo<int> foo;
            setup(&foo);
            return foo.sum;
        }
        int testZSetterDidSetY()
        {
            Foo<int> foo;
            foo.z = Bar<int>(53, 932);
            return foo.y[1];
        }
        int testZSetter()
        {
            Foo<int> foo;
            foo.z = Bar<int>(53, 932);
            return foo.sum;
        }
        int testZGetter()
        {
            Foo<int> foo;
            // This deliberately does not call setup() just so we test this syntax.
            foo.x = 1;
            foo.y[0] = 2;
            foo.y[1] = 3;
            foo.y[2] = 4;
            return foo.z.sum;
        }
        int testLValueEmulation()
        {
            Foo<int> foo;
            setup(&foo);
            foo.z.y *= 5;
            return foo.sum;
        }
    `);
    checkInt(program, callFunction(program, "testSuperBasic", [], []), 1 + 2 + 3 + 4);
    checkInt(program, callFunction(program, "testZSetterDidSetY", [], []), 932);
    checkInt(program, callFunction(program, "testZSetter", [], []), 53 + 932);
    checkInt(program, callFunction(program, "testZGetter", [], []), 1 + 3);
    checkInt(program, callFunction(program, "testLValueEmulation", [], []), 1 + 2 + 3 * 5 + 4);
}

tests.bitSubscriptAccessor = function()
{
    let program = doPrep(`
        protocol MyBitmaskable : Equatable {
            MyBitmaskable operator&(MyBitmaskable, MyBitmaskable);
            MyBitmaskable operator|(MyBitmaskable, MyBitmaskable);
            MyBitmaskable operator~(MyBitmaskable);
            MyBitmaskable operator<<(MyBitmaskable, uint);
            MyBitmaskable operator>>(MyBitmaskable, uint);
            operator MyBitmaskable(int);
        }
        T maskForBitIndex<T:MyBitmaskable>(uint index)
        {
            return T(1) << index;
        }
        bool operator[]<T:MyBitmaskable>(T value, uint index)
        {
            return bool(value & maskForBitIndex<T>(index));
        }
        T operator[]=<T:MyBitmaskable>(T value, uint index, bool bit)
        {
            T mask = maskForBitIndex<T>(index);
            if (bit)
                value |= mask;
            else
                value &= ~mask;
            return value;
        }
        uint operator.length(int)
        {
            return 32;
        }
        uint operator.length(uint)
        {
            return 32;
        }
        int testIntSetBit3()
        {
            int foo;
            foo[3] = true;
            return foo;
        }
        bool testIntSetGetBit5()
        {
            int foo;
            foo[5] = true;
            return foo[5];
        }
        bool testIntGetBit1()
        {
            int foo;
            return foo[1];
        }
        int testUintSumBits()
        {
            int foo = 42;
            int result;
            for (uint i = 0; i < foo.length; ++i) {
                if (foo[i])
                    result++;
            }
            return result;
        }
        int testUintSwapBits()
        {
            int foo = 42;
            for (uint i = 0; i < foo.length / 2; ++i) {
                bool tmp = foo[i];
                foo[i] = foo[foo.length - i - 1];
                foo[foo.length - i - 1] = tmp;
            }
            return foo;
        }
        struct Foo {
            uint f;
            uint g;
        }
        operator Foo(uint f, uint g)
        {
            Foo result;
            result.f = f;
            result.g = g;
            return result;
        }
        int operator.h(Foo foo)
        {
            return int((foo.f & 0xffff) | ((foo.g & 0xffff) << 16));
        }
        Foo operator.h=(Foo foo, int value)
        {
            foo.f &= ~0xffffu;
            foo.f |= uint(value) & 0xffff;
            foo.g &= ~0xffffu;
            foo.g |= (uint(value) >> 16) & 0xffff;
            return foo;
        }
        int testLValueEmulation()
        {
            Foo foo;
            foo.f = 42;
            foo.g = 37;
            for (uint i = 0; i < foo.h.length; ++i)
                foo.h[i] ^= true;
            return int(foo.f + foo.g);
        }
        struct Bar {
            Foo a;
            Foo b;
        }
        Foo operator.c(Bar bar)
        {
            return Foo(uint(bar.a.h), uint(bar.b.h));
        }
        Bar operator.c=(Bar bar, Foo foo)
        {
            bar.a.h = int(foo.f);
            bar.b.h = int(foo.g);
            return bar;
        }
        int testCrazyLValueEmulation()
        {
            Bar bar;
            bar.a.f = 1;
            bar.a.g = 2;
            bar.b.f = 3;
            bar.b.g = 4;
            for (uint i = 0; i < bar.c.h.length; i += 2)
                bar.c.h[i] ^= true;
            return int(bar.a.f + bar.a.g + bar.b.f + bar.b.g);
        }
    `);
    checkInt(program, callFunction(program, "testIntSetBit3", [], []), 8);
    checkBool(program, callFunction(program, "testIntSetGetBit5", [], []), true);
    checkBool(program, callFunction(program, "testIntGetBit1", [], []), false);
    checkInt(program, callFunction(program, "testUintSumBits", [], []), 3);
    checkInt(program, callFunction(program, "testUintSwapBits", [], []), 1409286144);
    checkInt(program, callFunction(program, "testLValueEmulation", [], []), 130991);
    checkInt(program, callFunction(program, "testCrazyLValueEmulation", [], []), 43696);
}

tests.nestedSubscriptLValueEmulationSimple = function()
{
    let program = doPrep(`
        struct Foo {
            int[7] array;
        }
        int operator[](Foo foo, uint index)
        {
            return foo.array[index];
        }
        Foo operator[]=(Foo foo, uint index, int value)
        {
            foo.array[index] = value;
            return foo;
        }
        uint operator.length(Foo foo)
        {
            return foo.array.length;
        }
        int sum(Foo foo)
        {
            int result = 0;
            for (uint i = foo.length; i--;)
                result += foo[i];
            return result;
        }
        struct Bar {
            Foo[6] array;
        }
        uint operator.length(Bar bar)
        {
            return bar.array.length;
        }
        Foo operator[](Bar bar, uint index)
        {
            return bar.array[index];
        }
        Bar operator[]=(Bar bar, uint index, Foo value)
        {
            bar.array[index] = value;
            return bar;
        }
        int sum(Bar bar)
        {
            int result = 0;
            for (uint i = bar.length; i--;)
                result += sum(bar[i]);
            return result;
        }
        struct Baz {
            Bar[5] array;
        }
        Bar operator[](Baz baz, uint index)
        {
            return baz.array[index];
        }
        Baz operator[]=(Baz baz, uint index, Bar value)
        {
            baz.array[index] = value;
            return baz;
        }
        uint operator.length(Baz baz)
        {
            return baz.array.length;
        }
        int sum(Baz baz)
        {
            int result = 0;
            for (uint i = baz.length; i--;)
                result += sum(baz[i]);
            return result;
        }
        void setValues(thread Baz* baz)
        {
            for (uint i = baz->length; i--;) {
                for (uint j = (*baz)[i].length; j--;) {
                    for (uint k = (*baz)[i][j].length; k--;)
                        (*baz)[i][j][k] = int(i + j + k);
                }
            }
        }
        int testSetValuesAndSum()
        {
            Baz baz;
            setValues(&baz);
            return sum(baz);
        }
        int testSetValuesMutateValuesAndSum()
        {
            Baz baz;
            setValues(&baz);
            for (uint i = baz.length; i--;) {
                for (uint j = baz[i].length; j--;) {
                    for (uint k = baz[i][j].length; k--;)
                        baz[i][j][k] *= int(k);
                }
            }
            return sum(baz);
        }
    `);
    checkInt(program, callFunction(program, "testSetValuesAndSum", [], []), 1575);
    checkInt(program, callFunction(program, "testSetValuesMutateValuesAndSum", [], []), 5565);
}

tests.nestedSubscriptLValueEmulationGeneric = function()
{
    let program = doPrep(`
        struct Foo<T> {
            T[7] array;
        }
        T operator[]<T>(Foo<T> foo, uint index)
        {
            return foo.array[index];
        }
        Foo<T> operator[]=<T>(Foo<T> foo, uint index, T value)
        {
            foo.array[index] = value;
            return foo;
        }
        uint operator.length<T>(Foo<T> foo)
        {
            return foo.array.length;
        }
        protocol MyAddable {
            MyAddable operator+(MyAddable, MyAddable);
        }
        T sum<T:MyAddable>(Foo<T> foo)
        {
            T result;
            for (uint i = foo.length; i--;)
                result += foo[i];
            return result;
        }
        struct Bar<T> {
            Foo<T>[6] array;
        }
        uint operator.length<T>(Bar<T> bar)
        {
            return bar.array.length;
        }
        Foo<T> operator[]<T>(Bar<T> bar, uint index)
        {
            return bar.array[index];
        }
        Bar<T> operator[]=<T>(Bar<T> bar, uint index, Foo<T> value)
        {
            bar.array[index] = value;
            return bar;
        }
        T sum<T:MyAddable>(Bar<T> bar)
        {
            T result;
            for (uint i = bar.length; i--;)
                result += sum(bar[i]);
            return result;
        }
        struct Baz<T> {
            Bar<T>[5] array;
        }
        Bar<T> operator[]<T>(Baz<T> baz, uint index)
        {
            return baz.array[index];
        }
        Baz<T> operator[]=<T>(Baz<T> baz, uint index, Bar<T> value)
        {
            baz.array[index] = value;
            return baz;
        }
        uint operator.length<T>(Baz<T> baz)
        {
            return baz.array.length;
        }
        T sum<T:MyAddable>(Baz<T> baz)
        {
            T result;
            for (uint i = baz.length; i--;)
                result += sum(baz[i]);
            return result;
        }
        protocol MyConvertibleFromUint {
            operator MyConvertibleFromUint(uint);
        }
        protocol SetValuable : MyAddable, MyConvertibleFromUint { }
        void setValues<T:SetValuable>(thread Baz<T>* baz)
        {
            for (uint i = baz->length; i--;) {
                for (uint j = (*baz)[i].length; j--;) {
                    for (uint k = (*baz)[i][j].length; k--;)
                        (*baz)[i][j][k] = T(i + j + k);
                }
            }
        }
        int testSetValuesAndSum()
        {
            Baz<int> baz;
            setValues(&baz);
            return sum(baz);
        }
        int testSetValuesMutateValuesAndSum()
        {
            Baz<int> baz;
            setValues(&baz);
            for (uint i = baz.length; i--;) {
                for (uint j = baz[i].length; j--;) {
                    for (uint k = baz[i][j].length; k--;)
                        baz[i][j][k] *= int(k);
                }
            }
            return sum(baz);
        }
    `);
    checkInt(program, callFunction(program, "testSetValuesAndSum", [], []), 1575);
    checkInt(program, callFunction(program, "testSetValuesMutateValuesAndSum", [], []), 5565);
}

tests.boolBitAnd = function()
{
    let program = doPrep(`
        bool foo(bool a, bool b)
        {
            return a & b;
        }
    `);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, false), makeBool(program, false)]), false);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, true), makeBool(program, false)]), false);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, false), makeBool(program, true)]), false);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, true), makeBool(program, true)]), true);
}

tests.boolBitOr = function()
{
    let program = doPrep(`
        bool foo(bool a, bool b)
        {
            return a | b;
        }
    `);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, false), makeBool(program, false)]), false);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, true), makeBool(program, false)]), true);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, false), makeBool(program, true)]), true);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, true), makeBool(program, true)]), true);
}

tests.boolBitXor = function()
{
    let program = doPrep(`
        bool foo(bool a, bool b)
        {
            return a ^ b;
        }
    `);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, false), makeBool(program, false)]), false);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, true), makeBool(program, false)]), true);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, false), makeBool(program, true)]), true);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, true), makeBool(program, true)]), false);
}

tests.boolBitNot = function()
{
    let program = doPrep(`
        bool foo(bool a)
        {
            return ~a;
        }
    `);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, false)]), true);
    checkBool(program, callFunction(program, "foo", [], [makeBool(program, true)]), false);
}

tests.intBitAnd = function()
{
    let program = doPrep(`
        int foo(int a, int b)
        {
            return a & b;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 1), makeInt(program, 7)]), 1);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 65535), makeInt(program, 42)]), 42);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, -1), makeInt(program, -7)]), -7);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 0), makeInt(program, 85732)]), 0);
}

tests.intBitOr = function()
{
    let program = doPrep(`
        int foo(int a, int b)
        {
            return a | b;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 1), makeInt(program, 7)]), 7);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 65535), makeInt(program, 42)]), 65535);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, -1), makeInt(program, -7)]), -1);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 0), makeInt(program, 85732)]), 85732);
}

tests.intBitXor = function()
{
    let program = doPrep(`
        int foo(int a, int b)
        {
            return a ^ b;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 1), makeInt(program, 7)]), 6);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 65535), makeInt(program, 42)]), 65493);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, -1), makeInt(program, -7)]), 6);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 0), makeInt(program, 85732)]), 85732);
}

tests.intBitNot = function()
{
    let program = doPrep(`
        int foo(int a)
        {
            return ~a;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 1)]), -2);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 65535)]), -65536);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, -1)]), 0);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 0)]), -1);
}

tests.intLShift = function()
{
    let program = doPrep(`
        int foo(int a, uint b)
        {
            return a << b;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 1), makeUint(program, 7)]), 128);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 65535), makeUint(program, 2)]), 262140);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, -1), makeUint(program, 5)]), -32);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 0), makeUint(program, 3)]), 0);
}

tests.intRShift = function()
{
    let program = doPrep(`
        int foo(int a, uint b)
        {
            return a >> b;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 1), makeUint(program, 7)]), 0);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 65535), makeUint(program, 2)]), 16383);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, -1), makeUint(program, 5)]), -1);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 0), makeUint(program, 3)]), 0);
}

tests.uintBitAnd = function()
{
    let program = doPrep(`
        uint foo(uint a, uint b)
        {
            return a & b;
        }
    `);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 1), makeUint(program, 7)]), 1);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 65535), makeUint(program, 42)]), 42);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, -1), makeUint(program, -7)]), 4294967289);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 0), makeUint(program, 85732)]), 0);
}

tests.uintBitOr = function()
{
    let program = doPrep(`
        uint foo(uint a, uint b)
        {
            return a | b;
        }
    `);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 1), makeUint(program, 7)]), 7);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 65535), makeUint(program, 42)]), 65535);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, -1), makeUint(program, -7)]), 4294967295);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 0), makeUint(program, 85732)]), 85732);
}

tests.uintBitXor = function()
{
    let program = doPrep(`
        uint foo(uint a, uint b)
        {
            return a ^ b;
        }
    `);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 1), makeUint(program, 7)]), 6);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 65535), makeUint(program, 42)]), 65493);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, -1), makeUint(program, -7)]), 6);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 0), makeUint(program, 85732)]), 85732);
}

tests.uintBitNot = function()
{
    let program = doPrep(`
        uint foo(uint a)
        {
            return ~a;
        }
    `);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 1)]), 4294967294);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 65535)]), 4294901760);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, -1)]), 0);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 0)]), 4294967295);
}

tests.uintLShift = function()
{
    let program = doPrep(`
        uint foo(uint a, uint b)
        {
            return a << b;
        }
    `);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 1), makeUint(program, 7)]), 128);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 65535), makeUint(program, 2)]), 262140);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, -1), makeUint(program, 5)]), 4294967264);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 0), makeUint(program, 3)]), 0);
}

tests.uintRShift = function()
{
    let program = doPrep(`
        uint foo(uint a, uint b)
        {
            return a >> b;
        }
    `);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 1), makeUint(program, 7)]), 0);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 65535), makeUint(program, 2)]), 16383);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, -1), makeUint(program, 5)]), 134217727);
    checkUint(program, callFunction(program, "foo", [], [makeUint(program, 0), makeUint(program, 3)]), 0);
}

tests.uint8BitAnd = function()
{
    let program = doPrep(`
        uint8 foo(uint8 a, uint8 b)
        {
            return a & b;
        }
    `);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 1), makeUint8(program, 7)]), 1);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 65535), makeUint8(program, 42)]), 42);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, -1), makeUint8(program, -7)]), 249);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 0), makeUint8(program, 85732)]), 0);
}

tests.uint8BitOr = function()
{
    let program = doPrep(`
        uint8 foo(uint8 a, uint8 b)
        {
            return a | b;
        }
    `);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 1), makeUint8(program, 7)]), 7);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 65535), makeUint8(program, 42)]), 255);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, -1), makeUint8(program, -7)]), 255);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 0), makeUint8(program, 85732)]), 228);
}

tests.uint8BitXor = function()
{
    let program = doPrep(`
        uint8 foo(uint8 a, uint8 b)
        {
            return a ^ b;
        }
    `);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 1), makeUint8(program, 7)]), 6);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 65535), makeUint8(program, 42)]), 213);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, -1), makeUint8(program, -7)]), 6);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 0), makeUint8(program, 85732)]), 228);
}

tests.uint8BitNot = function()
{
    let program = doPrep(`
        uint8 foo(uint8 a)
        {
            return ~a;
        }
    `);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 1)]), 254);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 65535)]), 0);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, -1)]), 0);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 0)]), 255);
}

tests.uint8LShift = function()
{
    let program = doPrep(`
        uint8 foo(uint8 a, uint b)
        {
            return a << b;
        }
    `);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 1), makeUint(program, 7)]), 128);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 65535), makeUint(program, 2)]), 252);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, -1), makeUint(program, 5)]), 224);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 0), makeUint(program, 3)]), 0);
}

tests.uint8RShift = function()
{
    let program = doPrep(`
        uint8 foo(uint8 a, uint b)
        {
            return a >> b;
        }
    `);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 1), makeUint(program, 7)]), 0);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 65535), makeUint(program, 2)]), 255);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, -1), makeUint(program, 5)]), 255);
    checkUint8(program, callFunction(program, "foo", [], [makeUint8(program, 0), makeUint(program, 3)]), 0);
}

tests.floatMath = function()
{
    let program = doPrep(`
        bool foo()
        {
            return 42.5 == 42.5;
        }
        bool foo2()
        {
            return 42.5f == 42.5;
        }
        bool foo3()
        {
            return 42.5 == 42.5f;
        }
        bool foo4()
        {
            return 42.5f == 42.5f;
        }
        bool foo5()
        {
            return 42.5d == 42.5d;
        }
        float bar(float x)
        {
            return x;
        }
        float foo6()
        {
            return bar(7.5);
        }
        float foo7()
        {
            return bar(7.5f);
        }
        float foo8()
        {
            return bar(7.5d);
        }
        float foo9()
        {
            return float(7.5);
        }
        float foo10()
        {
            return float(7.5f);
        }
        float foo11()
        {
            return float(7.5d);
        }
        float foo12()
        {
            return float(7);
        }
        float foo13()
        {
            double x = 7.5d;
            return float(x);
        }
        double foo14()
        {
            double x = 7.5f;
            return double(x);
        }
    `);
    checkBool(program, callFunction(program, "foo", [], []), true);
    checkBool(program, callFunction(program, "foo2", [], []), true);
    checkBool(program, callFunction(program, "foo3", [], []), true);
    checkBool(program, callFunction(program, "foo4", [], []), true);
    checkBool(program, callFunction(program, "foo5", [], []), true);
    checkFloat(program, callFunction(program, "foo6", [], []), 7.5);
    checkFloat(program, callFunction(program, "foo7", [], []), 7.5);
    checkFloat(program, callFunction(program, "foo8", [], []), 7.5);
    checkFloat(program, callFunction(program, "foo9", [], []), 7.5);
    checkFloat(program, callFunction(program, "foo10", [], []), 7.5);
    checkFloat(program, callFunction(program, "foo11", [], []), 7.5);
    checkFloat(program, callFunction(program, "foo12", [], []), 7);
    checkFloat(program, callFunction(program, "foo13", [], []), 7.5);
    checkDouble(program, callFunction(program, "foo14", [], []), 7.5);
    checkFail(
        () => doPrep(`
            int bar(int x)
            {
                return x;
            }
            int foo()
            {
                bar(4.);
            }
        `),
        (e) => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int bar(int x)
            {
                return x;
            }
            int foo()
            {
                bar(4.d);
            }
        `),
        (e) => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int bar(int x)
            {
                return x;
            }
            int foo()
            {
                bar(4.f);
            }
        `),
        (e) => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            uint bar(uint x)
            {
                return x;
            }
            int foo()
            {
                bar(4.);
            }
        `),
        (e) => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            uint bar(uint x)
            {
                return x;
            }
            int foo()
            {
                bar(4.d);
            }
        `),
        (e) => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            uint bar(uint x)
            {
                return x;
            }
            int foo()
            {
                bar(4.f);
            }
        `),
        (e) => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            float bar(float x)
            {
                return x;
            }
            void foo()
            {
                bar(16777217.d);
            }
        `),
        (e) => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            float bar(float x)
            {
                return x;
            }
            float foo()
            {
                double x = 7.;
                return bar(x);
            }
        `),
        (e) => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            float foo()
            {
                double x = 7.;
                return x;
            }
        `),
        (e) => e instanceof WTypeError);
}

tests.genericCastInfer = function()
{
    let program = doPrep(`
        struct Complex<T> {
            T real;
            T imag;
        }
        operator<T> Complex<T>(T real, T imag)
        {
            Complex<T> result;
            result.real = real;
            result.imag = imag;
            return result;
        }
        int foo()
        {
            Complex<int> x = Complex<int>(1, 2);
            return x.real + x.imag;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 3);
}

tests.booleanMath = function()
{
    let program = doPrep(`
        bool foo()
        {
            return true && true;
        }
        bool foo2()
        {
            return true && false;
        }
        bool foo3()
        {
            return false && true;
        }
        bool foo4()
        {
            return false && false;
        }
        bool foo5()
        {
            return true || true;
        }
        bool foo6()
        {
            return true || false;
        }
        bool foo7()
        {
            return false || true;
        }
        bool foo8()
        {
            return false || false;
        }
    `);
    checkBool(program, callFunction(program, "foo", [], []), true);
    checkBool(program, callFunction(program, "foo2", [], []), false);
    checkBool(program, callFunction(program, "foo3", [], []), false);
    checkBool(program, callFunction(program, "foo4", [], []), false);
    checkBool(program, callFunction(program, "foo5", [], []), true);
    checkBool(program, callFunction(program, "foo6", [], []), true);
    checkBool(program, callFunction(program, "foo7", [], []), true);
    checkBool(program, callFunction(program, "foo8", [], []), false);
}

tests.typedefArray = function()
{
    let program = doPrep(`
        typedef ArrayTypedef = int[2];
        int foo()
        {
            ArrayTypedef arrayTypedef;
            return arrayTypedef[0];
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 0);
}

tests.shaderTypes = function()
{
    checkFail(
        () => doPrep(`
            struct Foo {
                float4 x;
            }
            vertex Foo bar()
            {
                Foo result;
                result.x = float4();
                return result;
            }
            Foo foo() {
                return bar();
            }
        `),
        (e) => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            vertex float bar()
            {
                return 4.;
            }
        `),
        (e) => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            struct Foo {
                float4 x;
            }
            vertex Foo bar(device Foo* x)
            {
                return Foo();
            }
        `),
        (e) => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            struct Boo {
                float4 x;
            }
            struct Foo {
                float4 x;
                device Boo* y;
            }
            vertex Foo bar()
            {
                return Foo();
            }
        `),
        (e) => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            struct Foo {
                float4 x;
            }
            struct Boo {
                device Foo* y;
            }
            vertex Foo bar(Boo b)
            {
                return Foo();
            }
        `),
        (e) => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            struct Foo {
                float4 x;
            }
            vertex Foo bar(device Foo* x)
            {
                return Foo();
            }
        `),
        (e) => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            struct Foo {
                float4 x;
            }
            fragment Foo bar(Foo foo)
            {
                return Foo();
            }
        `),
        (e) => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            struct Foo {
                float4 x;
            }
            fragment Foo bar(device Foo* stageIn)
            {
                return Foo();
            }
        `),
        (e) => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            struct Boo {
                float4 x;
            }
            struct Foo {
                float4 x;
                device Boo* y;
            }
            fragment Boo bar(Foo stageIn)
            {
                return boo();
            }
        `),
        (e) => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            struct Boo {
                float4 x;
            }
            struct Foo {
                float4 x;
                device Boo* y;
            }
            fragment Foo bar(Boo stageIn)
            {
                return Foo();
            }
        `),
        (e) => e instanceof WTypeError);
}

tests.builtinVectors = function()
{
    let program = doPrep(`
        int foo()
        {
            int2 a = int2(3, 4);
            return a[0];
        }
        int foo2()
        {
            int2 a = int2(3, 4);
            int3 b = int3(a, 5);
            return b[1];
        }
        int foo3()
        {
            int3 a = int3(3, 4, 5);
            int4 b = int4(6, a);
            return b[1];
        }
        int foo4()
        {
            int2 a = int2(3, 4);
            int2 b = int2(5, 6);
            int4 c = int4(a, b);
            return c[2];
        }
        bool foo5()
        {
            int4 a = int4(3, 4, 5, 6);
            int2 b = int2(4, 5);
            int4 c = int4(3, b, 6);
            return a == c;
        }
        bool foo6()
        {
            int2 a = int2(4, 5);
            int3 b = int3(3, a);
            int3 c = int3(3, 4, 6);
            return b == c;
        }
        uint foou()
        {
            uint2 a = uint2(3, 4);
            return a[0];
        }
        uint foou2()
        {
            uint2 a = uint2(3, 4);
            uint3 b = uint3(a, 5);
            return b[1];
        }
        uint foou3()
        {
            uint3 a = uint3(3, 4, 5);
            uint4 b = uint4(6, a);
            return b[1];
        }
        uint foou4()
        {
            uint2 a = uint2(3, 4);
            uint2 b = uint2(5, 6);
            uint4 c = uint4(a, b);
            return c[2];
        }
        bool foou5()
        {
            uint4 a = uint4(3, 4, 5, 6);
            uint2 b = uint2(4, 5);
            uint4 c = uint4(3, b, 6);
            return a == c;
        }
        bool foou6()
        {
            uint2 a = uint2(4, 5);
            uint3 b = uint3(3, a);
            uint3 c = uint3(3, 4, 6);
            return b == c;
        }
        float foof()
        {
            float2 a = float2(3., 4.);
            return a[0];
        }
        float foof2()
        {
            float2 a = float2(3., 4.);
            float3 b = float3(a, 5.);
            return b[1];
        }
        float foof3()
        {
            float3 a = float3(3., 4., 5.);
            float4 b = float4(6., a);
            return b[1];
        }
        float foof4()
        {
            float2 a = float2(3., 4.);
            float2 b = float2(5., 6.);
            float4 c = float4(a, b);
            return c[2];
        }
        bool foof5()
        {
            float4 a = float4(3., 4., 5., 6.);
            float2 b = float2(4., 5.);
            float4 c = float4(3., b, 6.);
            return a == c;
        }
        bool foof6()
        {
            float2 a = float2(4., 5.);
            float3 b = float3(3., a);
            float3 c = float3(3., 4., 6.);
            return b == c;
        }
        double food()
        {
            double2 a = double2(3., 4.);
            return a[0];
        }
        double food2()
        {
            double2 a = double2(3., 4.);
            double3 b = double3(a, 5.);
            return b[1];
        }
        double food3()
        {
            double3 a = double3(3., 4., 5.);
            double4 b = double4(6., a);
            return b[1];
        }
        double food4()
        {
            double2 a = double2(3., 4.);
            double2 b = double2(5., 6.);
            double4 c = double4(a, b);
            return c[2];
        }
        bool food5()
        {
            double4 a = double4(3., 4., 5., 6.);
            double2 b = double2(4., 5.);
            double4 c = double4(3., b, 6.);
            return a == c;
        }
        bool food6()
        {
            double2 a = double2(4., 5.);
            double3 b = double3(3., a);
            double3 c = double3(3., 4., 6.);
            return b == c;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 3);
    checkInt(program, callFunction(program, "foo2", [], []), 4);
    checkInt(program, callFunction(program, "foo3", [], []), 3);
    checkInt(program, callFunction(program, "foo4", [], []), 5);
    checkBool(program, callFunction(program, "foo5", [], []), true);
    checkBool(program, callFunction(program, "foo6", [], []), false);
    checkUint(program, callFunction(program, "foou", [], []), 3);
    checkUint(program, callFunction(program, "foou2", [], []), 4);
    checkUint(program, callFunction(program, "foou3", [], []), 3);
    checkUint(program, callFunction(program, "foou4", [], []), 5);
    checkBool(program, callFunction(program, "foou5", [], []), true);
    checkBool(program, callFunction(program, "foou6", [], []), false);
    checkFloat(program, callFunction(program, "foof", [], []), 3);
    checkFloat(program, callFunction(program, "foof2", [], []), 4);
    checkFloat(program, callFunction(program, "foof3", [], []), 3);
    checkFloat(program, callFunction(program, "foof4", [], []), 5);
    checkBool(program, callFunction(program, "foof5", [], []), true);
    checkBool(program, callFunction(program, "foof6", [], []), false);
    checkDouble(program, callFunction(program, "food", [], []), 3);
    checkDouble(program, callFunction(program, "food2", [], []), 4);
    checkDouble(program, callFunction(program, "food3", [], []), 3);
    checkDouble(program, callFunction(program, "food4", [], []), 5);
    checkBool(program, callFunction(program, "food5", [], []), true);
    checkBool(program, callFunction(program, "food6", [], []), false);
}

tests.instantiateStructInStruct = function()
{
    let program = doPrep(`
        struct Bar<T> {
            T x;
        }
        struct Foo {
            Bar<int> x;
        }
        int foo()
        {
            Foo x;
            x.x.x = 42;
            x.x.x++;
            return x.x.x;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 43);
}

tests.instantiateStructInStructWithInt2 = function()
{
    let program = doPrep(`
        struct Foo {
            int2 x;
        }
        int foo()
        {
            Foo x;
            x.x.x = 42;
            x.x.x++;
            return x.x.x;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 43);
}

tests.simpleEnum = function()
{
    let program = doPrep(`
        enum Foo {
            War,
            Famine,
            Pestilence,
            Death
        }
        Foo war()
        {
            return Foo.War;
        }
        Foo famine()
        {
            return Foo.Famine;
        }
        Foo pestilence()
        {
            return Foo.Pestilence;
        }
        Foo death()
        {
            return Foo.Death;
        }
        bool equals(Foo a, Foo b)
        {
            return a == b;
        }
        bool notEquals(Foo a, Foo b)
        {
            return a != b;
        }
        bool testSimpleEqual()
        {
            return equals(Foo.War, Foo.War);
        }
        bool testAnotherEqual()
        {
            return equals(Foo.Pestilence, Foo.Pestilence);
        }
        bool testNotEqual()
        {
            return equals(Foo.Famine, Foo.Death);
        }
        bool testSimpleNotEqual()
        {
            return notEquals(Foo.War, Foo.War);
        }
        bool testAnotherNotEqual()
        {
            return notEquals(Foo.Pestilence, Foo.Pestilence);
        }
        bool testNotNotEqual()
        {
            return notEquals(Foo.Famine, Foo.Death);
        }
        int intWar()
        {
            return int(war());
        }
        int intFamine()
        {
            return int(famine());
        }
        int intPestilence()
        {
            return int(pestilence());
        }
        int intDeath()
        {
            return int(death());
        }
        int warValue()
        {
            return war().value;
        }
        int famineValue()
        {
            return famine().value;
        }
        int pestilenceValue()
        {
            return pestilence().value;
        }
        int deathValue()
        {
            return death().value;
        }
        int warValueLiteral()
        {
            return Foo.War.value;
        }
        int famineValueLiteral()
        {
            return Foo.Famine.value;
        }
        int pestilenceValueLiteral()
        {
            return Foo.Pestilence.value;
        }
        int deathValueLiteral()
        {
            return Foo.Death.value;
        }
        Foo intWarBackwards()
        {
            return Foo(intWar());
        }
        Foo intFamineBackwards()
        {
            return Foo(intFamine());
        }
        Foo intPestilenceBackwards()
        {
            return Foo(intPestilence());
        }
        Foo intDeathBackwards()
        {
            return Foo(intDeath());
        }
    `);
    checkEnum(program, callFunction(program, "war", [], []), 0);
    checkEnum(program, callFunction(program, "famine", [], []), 1);
    checkEnum(program, callFunction(program, "pestilence", [], []), 2);
    checkEnum(program, callFunction(program, "death", [], []), 3);
    checkBool(program, callFunction(program, "testSimpleEqual", [], []), true);
    checkBool(program, callFunction(program, "testAnotherEqual", [], []), true);
    checkBool(program, callFunction(program, "testNotEqual", [], []), false);
    checkBool(program, callFunction(program, "testSimpleNotEqual", [], []), false);
    checkBool(program, callFunction(program, "testAnotherNotEqual", [], []), false);
    checkBool(program, callFunction(program, "testNotNotEqual", [], []), true);
    checkInt(program, callFunction(program, "intWar", [], []), 0);
    checkInt(program, callFunction(program, "intFamine", [], []), 1);
    checkInt(program, callFunction(program, "intPestilence", [], []), 2);
    checkInt(program, callFunction(program, "intDeath", [], []), 3);
    checkInt(program, callFunction(program, "warValue", [], []), 0);
    checkInt(program, callFunction(program, "famineValue", [], []), 1);
    checkInt(program, callFunction(program, "pestilenceValue", [], []), 2);
    checkInt(program, callFunction(program, "deathValue", [], []), 3);
    checkInt(program, callFunction(program, "warValueLiteral", [], []), 0);
    checkInt(program, callFunction(program, "famineValueLiteral", [], []), 1);
    checkInt(program, callFunction(program, "pestilenceValueLiteral", [], []), 2);
    checkInt(program, callFunction(program, "deathValueLiteral", [], []), 3);
    checkEnum(program, callFunction(program, "intWarBackwards", [], []), 0);
    checkEnum(program, callFunction(program, "intFamineBackwards", [], []), 1);
    checkEnum(program, callFunction(program, "intPestilenceBackwards", [], []), 2);
    checkEnum(program, callFunction(program, "intDeathBackwards", [], []), 3);
}

tests.enumWithManualValues = function()
{
    let program = doPrep(`
        enum Foo {
            War = 72,
            Famine = 0,
            Pestilence = 23,
            Death = -42
        }
        Foo war()
        {
            return Foo.War;
        }
        Foo famine()
        {
            return Foo.Famine;
        }
        Foo pestilence()
        {
            return Foo.Pestilence;
        }
        Foo death()
        {
            return Foo.Death;
        }
    `);
    checkEnum(program, callFunction(program, "war", [], []), 72);
    checkEnum(program, callFunction(program, "famine", [], []), 0);
    checkEnum(program, callFunction(program, "pestilence", [], []), 23);
    checkEnum(program, callFunction(program, "death", [], []), -42);
}

tests.enumWithoutZero = function()
{
    checkFail(
        () => doPrep(`
            enum Foo {
                War = 72,
                Famine = 64,
                Pestilence = 23,
                Death = -42
            }
        `),
        e => e instanceof WTypeError);
}

tests.enumDuplicates = function()
{
    checkFail(
        () => doPrep(`
            enum Foo {
                War = -42,
                Famine = 0,
                Pestilence = 23,
                Death = -42
            }
        `),
        e => e instanceof WTypeError);
}

tests.enumWithSomeManualValues = function()
{
    let program = doPrep(`
        enum Foo {
            War = 72,
            Famine,
            Pestilence = 0,
            Death
        }
        Foo war()
        {
            return Foo.War;
        }
        Foo famine()
        {
            return Foo.Famine;
        }
        Foo pestilence()
        {
            return Foo.Pestilence;
        }
        Foo death()
        {
            return Foo.Death;
        }
    `);
    checkEnum(program, callFunction(program, "war", [], []), 72);
    checkEnum(program, callFunction(program, "famine", [], []), 73);
    checkEnum(program, callFunction(program, "pestilence", [], []), 0);
    checkEnum(program, callFunction(program, "death", [], []), 1);
}

tests.enumConstexprGenericFunction = function()
{
    let program = doPrep(`
        enum Axis { X, Y }
        int foo<Axis axis>() { return int(axis); }
        int testX() { return foo<Axis.X>(); }
        int testY() { return foo<Axis.Y>(); }
    `);
    checkInt(program, callFunction(program, "testX", [], []), 0);
    checkInt(program, callFunction(program, "testY", [], []), 1);
}

tests.enumConstexprGenericStruct = function()
{
    let program = doPrep(`
        enum Axis { X, Y }
        struct Foo<Axis axis> { }
        int foo<Axis axis>(Foo<axis>) { return int(axis); }
        int testX()
        {
            Foo<Axis.X> f;
            return foo(f);
        }
        int testY()
        {
            Foo<Axis.Y> f;
            return foo(f);
        }
    `);
    checkInt(program, callFunction(program, "testX", [], []), 0);
    checkInt(program, callFunction(program, "testY", [], []), 1);
}

tests.trap = function()
{
    let program = doPrep(`
        int foo()
        {
            trap;
        }
        int foo2(int x)
        {
            if (x == 3)
                trap;
            return 4;
        }
        struct Bar {
            int3 x;
            float y;
        }
        Bar foo3()
        {
            trap;
        }
    `);
    checkFail(
        () => callFunction(program, "foo", [], []),
        e => e instanceof WTrapError);
    checkInt(program, callFunction(program, "foo2", [], [makeInt(program, 1)]), 4);
    checkFail(
        () => callFunction(program, "foo2", [], [makeInt(program, 3)]),
        e => e instanceof WTrapError);
    checkFail(
        () => callFunction(program, "foo3", [], []),
        e => e instanceof WTrapError);
}

tests.swizzle = function()
{
    let program = doPrep(`
        float foo() {
            float4 bar = float4(3., 4., 5., 6.);
            float3 baz = bar.zzx;
            return baz.z;
        }
        float foo2() {
            float4 bar = float4(3., 4., 5., 6.);
            float3 baz = bar.wyz;
            return baz.x;
        }
        float foo3() {
            float3 bar = float3(3., 4., 5.);
            float2 baz = bar.yz;
            float4 quix = baz.yyxx;
            return quix.z;
        }
    `);
    checkFloat(program, callFunction(program, "foo", [], []), 3);
    checkFloat(program, callFunction(program, "foo2", [], []), 6);
    checkFloat(program, callFunction(program, "foo3", [], []), 4);
}

tests.enumWithExplicitIntBase = function()
{
    let program = doPrep(`
        enum Foo : int {
            War,
            Famine,
            Pestilence,
            Death
        }
        Foo war()
        {
            return Foo.War;
        }
        Foo famine()
        {
            return Foo.Famine;
        }
        Foo pestilence()
        {
            return Foo.Pestilence;
        }
        Foo death()
        {
            return Foo.Death;
        }
        bool equals(Foo a, Foo b)
        {
            return a == b;
        }
        bool notEquals(Foo a, Foo b)
        {
            return a != b;
        }
        bool testSimpleEqual()
        {
            return equals(Foo.War, Foo.War);
        }
        bool testAnotherEqual()
        {
            return equals(Foo.Pestilence, Foo.Pestilence);
        }
        bool testNotEqual()
        {
            return equals(Foo.Famine, Foo.Death);
        }
        bool testSimpleNotEqual()
        {
            return notEquals(Foo.War, Foo.War);
        }
        bool testAnotherNotEqual()
        {
            return notEquals(Foo.Pestilence, Foo.Pestilence);
        }
        bool testNotNotEqual()
        {
            return notEquals(Foo.Famine, Foo.Death);
        }
        int intWar()
        {
            return int(war());
        }
        int intFamine()
        {
            return int(famine());
        }
        int intPestilence()
        {
            return int(pestilence());
        }
        int intDeath()
        {
            return int(death());
        }
        int warValue()
        {
            return war().value;
        }
        int famineValue()
        {
            return famine().value;
        }
        int pestilenceValue()
        {
            return pestilence().value;
        }
        int deathValue()
        {
            return death().value;
        }
        int warValueLiteral()
        {
            return Foo.War.value;
        }
        int famineValueLiteral()
        {
            return Foo.Famine.value;
        }
        int pestilenceValueLiteral()
        {
            return Foo.Pestilence.value;
        }
        int deathValueLiteral()
        {
            return Foo.Death.value;
        }
        Foo intWarBackwards()
        {
            return Foo(intWar());
        }
        Foo intFamineBackwards()
        {
            return Foo(intFamine());
        }
        Foo intPestilenceBackwards()
        {
            return Foo(intPestilence());
        }
        Foo intDeathBackwards()
        {
            return Foo(intDeath());
        }
    `);
    checkEnum(program, callFunction(program, "war", [], []), 0);
    checkEnum(program, callFunction(program, "famine", [], []), 1);
    checkEnum(program, callFunction(program, "pestilence", [], []), 2);
    checkEnum(program, callFunction(program, "death", [], []), 3);
    checkBool(program, callFunction(program, "testSimpleEqual", [], []), true);
    checkBool(program, callFunction(program, "testAnotherEqual", [], []), true);
    checkBool(program, callFunction(program, "testNotEqual", [], []), false);
    checkBool(program, callFunction(program, "testSimpleNotEqual", [], []), false);
    checkBool(program, callFunction(program, "testAnotherNotEqual", [], []), false);
    checkBool(program, callFunction(program, "testNotNotEqual", [], []), true);
    checkInt(program, callFunction(program, "intWar", [], []), 0);
    checkInt(program, callFunction(program, "intFamine", [], []), 1);
    checkInt(program, callFunction(program, "intPestilence", [], []), 2);
    checkInt(program, callFunction(program, "intDeath", [], []), 3);
    checkInt(program, callFunction(program, "warValue", [], []), 0);
    checkInt(program, callFunction(program, "famineValue", [], []), 1);
    checkInt(program, callFunction(program, "pestilenceValue", [], []), 2);
    checkInt(program, callFunction(program, "deathValue", [], []), 3);
    checkInt(program, callFunction(program, "warValueLiteral", [], []), 0);
    checkInt(program, callFunction(program, "famineValueLiteral", [], []), 1);
    checkInt(program, callFunction(program, "pestilenceValueLiteral", [], []), 2);
    checkInt(program, callFunction(program, "deathValueLiteral", [], []), 3);
    checkEnum(program, callFunction(program, "intWarBackwards", [], []), 0);
    checkEnum(program, callFunction(program, "intFamineBackwards", [], []), 1);
    checkEnum(program, callFunction(program, "intPestilenceBackwards", [], []), 2);
    checkEnum(program, callFunction(program, "intDeathBackwards", [], []), 3);
}

tests.enumWithUintBase = function()
{
    let program = doPrep(`
        enum Foo : uint {
            War,
            Famine,
            Pestilence,
            Death
        }
        Foo war()
        {
            return Foo.War;
        }
        Foo famine()
        {
            return Foo.Famine;
        }
        Foo pestilence()
        {
            return Foo.Pestilence;
        }
        Foo death()
        {
            return Foo.Death;
        }
        bool equals(Foo a, Foo b)
        {
            return a == b;
        }
        bool notEquals(Foo a, Foo b)
        {
            return a != b;
        }
        bool testSimpleEqual()
        {
            return equals(Foo.War, Foo.War);
        }
        bool testAnotherEqual()
        {
            return equals(Foo.Pestilence, Foo.Pestilence);
        }
        bool testNotEqual()
        {
            return equals(Foo.Famine, Foo.Death);
        }
        bool testSimpleNotEqual()
        {
            return notEquals(Foo.War, Foo.War);
        }
        bool testAnotherNotEqual()
        {
            return notEquals(Foo.Pestilence, Foo.Pestilence);
        }
        bool testNotNotEqual()
        {
            return notEquals(Foo.Famine, Foo.Death);
        }
        uint uintWar()
        {
            return uint(war());
        }
        uint uintFamine()
        {
            return uint(famine());
        }
        uint uintPestilence()
        {
            return uint(pestilence());
        }
        uint uintDeath()
        {
            return uint(death());
        }
        uint warValue()
        {
            return war().value;
        }
        uint famineValue()
        {
            return famine().value;
        }
        uint pestilenceValue()
        {
            return pestilence().value;
        }
        uint deathValue()
        {
            return death().value;
        }
        uint warValueLiteral()
        {
            return Foo.War.value;
        }
        uint famineValueLiteral()
        {
            return Foo.Famine.value;
        }
        uint pestilenceValueLiteral()
        {
            return Foo.Pestilence.value;
        }
        uint deathValueLiteral()
        {
            return Foo.Death.value;
        }
        Foo uintWarBackwards()
        {
            return Foo(uintWar());
        }
        Foo uintFamineBackwards()
        {
            return Foo(uintFamine());
        }
        Foo uintPestilenceBackwards()
        {
            return Foo(uintPestilence());
        }
        Foo uintDeathBackwards()
        {
            return Foo(uintDeath());
        }
    `);
    checkEnum(program, callFunction(program, "war", [], []), 0);
    checkEnum(program, callFunction(program, "famine", [], []), 1);
    checkEnum(program, callFunction(program, "pestilence", [], []), 2);
    checkEnum(program, callFunction(program, "death", [], []), 3);
    checkBool(program, callFunction(program, "testSimpleEqual", [], []), true);
    checkBool(program, callFunction(program, "testAnotherEqual", [], []), true);
    checkBool(program, callFunction(program, "testNotEqual", [], []), false);
    checkBool(program, callFunction(program, "testSimpleNotEqual", [], []), false);
    checkBool(program, callFunction(program, "testAnotherNotEqual", [], []), false);
    checkBool(program, callFunction(program, "testNotNotEqual", [], []), true);
    checkUint(program, callFunction(program, "uintWar", [], []), 0);
    checkUint(program, callFunction(program, "uintFamine", [], []), 1);
    checkUint(program, callFunction(program, "uintPestilence", [], []), 2);
    checkUint(program, callFunction(program, "uintDeath", [], []), 3);
    checkUint(program, callFunction(program, "warValue", [], []), 0);
    checkUint(program, callFunction(program, "famineValue", [], []), 1);
    checkUint(program, callFunction(program, "pestilenceValue", [], []), 2);
    checkUint(program, callFunction(program, "deathValue", [], []), 3);
    checkUint(program, callFunction(program, "warValueLiteral", [], []), 0);
    checkUint(program, callFunction(program, "famineValueLiteral", [], []), 1);
    checkUint(program, callFunction(program, "pestilenceValueLiteral", [], []), 2);
    checkUint(program, callFunction(program, "deathValueLiteral", [], []), 3);
    checkEnum(program, callFunction(program, "uintWarBackwards", [], []), 0);
    checkEnum(program, callFunction(program, "uintFamineBackwards", [], []), 1);
    checkEnum(program, callFunction(program, "uintPestilenceBackwards", [], []), 2);
    checkEnum(program, callFunction(program, "uintDeathBackwards", [], []), 3);
}

tests.enumFloatBase = function()
{
    checkFail(
        () => doPrep(`
            enum Foo : float {
                Bar
            }
        `),
        e => e instanceof WTypeError);
}

tests.enumPtrBase = function()
{
    checkFail(
        () => doPrep(`
            enum Foo : thread int* {
                Bar
            }
        `),
        e => e instanceof WTypeError);
}

tests.enumArrayRefBase = function()
{
    checkFail(
        () => doPrep(`
            enum Foo : thread int[] {
                Bar
            }
        `),
        e => e instanceof WTypeError);
}

tests.emptyStruct = function()
{
    let program = doPrep(`
        struct Thingy { }
        int foo()
        {
            Thingy thingy;
            return 46;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 46);
}

tests.enumStructBase = function()
{
    checkFail(
        () => doPrep(`
            struct Thingy { }
            enum Foo : Thingy {
                Bar
            }
        `),
        e => e instanceof WTypeError);
}

tests.enumNoMembers = function()
{
    checkFail(
        () => doPrep(`
            enum Foo { }
        `),
        e => e instanceof WTypeError);
}

tests.simpleSwitch = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            switch (x) {
            case 767:
                return 27;
            case 69:
                return 7624;
            default:
                return 49;
            }
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 767)]), 27);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 69)]), 7624);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 0)]), 49);
}

tests.exhaustiveUint8Switch = function()
{
    let text = "double foo(uint8 x) { switch (uint8(x)) {"
    for (let i = 0; i <= 0xff; ++i)
        text += "case " + i + ": return " + i * 1.5 + ";";
    text += "} }";
    let program = doPrep(text);
    for (let i = 0; i < 0xff; ++i)
        checkDouble(program, callFunction(program, "foo", [], [makeUint8(program, i)]), i * 1.5);
}

tests.notQuiteExhaustiveUint8Switch = function()
{
    let text = "double foo(uint8 x) { switch (uint8(x)) {"
    for (let i = 0; i <= 0xfe; ++i)
        text += "case " + i + ": return " + i * 1.5 + ";";
    text += "} }";
    checkFail(() => doPrep(text), e => e instanceof WTypeError);
}

tests.notQuiteExhaustiveUint8SwitchWithDefault = function()
{
    let text = "double foo(uint8 x) { switch (uint8(x)) {"
    for (let i = 0; i <= 0xfe; ++i)
        text += "case " + i + ": return " + i * 1.5 + ";";
    text += "default: return " + 0xff * 1.5 + ";";
    text += "} }";
    let program = doPrep(text);
    for (let i = 0; i < 0xff; ++i)
        checkDouble(program, callFunction(program, "foo", [], [makeUint8(program, i)]), i * 1.5);
}

tests.switchFallThrough = function()
{
    // FIXME: This might become an error in future versions.
    // https://bugs.webkit.org/show_bug.cgi?id=177172
    let program = doPrep(`
        int foo(int x)
        {
            int result = 0;
            switch (x) {
            case 767:
                result += 27;
            case 69:
                result += 7624;
            default:
                result += 49;
            }
            return result;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 767)]), 27 + 7624 + 49);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 69)]), 7624 + 49);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 0)]), 49);
}

tests.switchBreak = function()
{
    let program = doPrep(`
        int foo(int x)
        {
            int result = 0;
            switch (x) {
            case 767:
                result += 27;
                break;
            case 69:
                result += 7624;
                break;
            default:
                result += 49;
                break;
            }
            return result;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 767)]), 27);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 69)]), 7624);
    checkInt(program, callFunction(program, "foo", [], [makeInt(program, 0)]), 49);
}

tests.enumSwitchBreakExhaustive = function()
{
    let program = doPrep(`
        enum Foo {
            A, B, C
        }
        int foo(Foo x)
        {
            int result = 0;
            switch (x) {
            case Foo.A:
                result += 27;
                break;
            case Foo.B:
                result += 7624;
                break;
            case Foo.C:
                result += 49;
                break;
            }
            return result;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeEnum(program, "Foo", "A")]), 27);
    checkInt(program, callFunction(program, "foo", [], [makeEnum(program, "Foo", "B")]), 7624);
    checkInt(program, callFunction(program, "foo", [], [makeEnum(program, "Foo", "C")]), 49);
}

tests.enumSwitchBreakNotQuiteExhaustive = function()
{
    checkFail(
        () => doPrep(`
            enum Foo {
                A, B, C, D
            }
            int foo(Foo x)
            {
                int result = 0;
                switch (x) {
                case Foo.A:
                    result += 27;
                    break;
                case Foo.B:
                    result += 7624;
                    break;
                case Foo.C:
                    result += 49;
                    break;
                }
                return result;
            }
        `),
        e => e instanceof WTypeError);
}

tests.enumSwitchBreakNotQuiteExhaustiveWithDefault = function()
{
    let program = doPrep(`
        enum Foo {
            A, B, C
        }
        int foo(Foo x)
        {
            int result = 0;
            switch (x) {
            case Foo.A:
                result += 27;
                break;
            case Foo.B:
                result += 7624;
                break;
            default:
                result += 49;
                break;
            }
            return result;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], [makeEnum(program, "Foo", "A")]), 27);
    checkInt(program, callFunction(program, "foo", [], [makeEnum(program, "Foo", "B")]), 7624);
    checkInt(program, callFunction(program, "foo", [], [makeEnum(program, "Foo", "C")]), 49);
}

tests.simpleRecursiveStruct = function()
{
    checkFail(
        () => doPrep(`
            struct Foo {
                Foo foo;
            }
        `),
        e => e instanceof WTypeError);
}

tests.mutuallyRecursiveStruct = function()
{
    checkFail(
        () => doPrep(`
            struct Foo {
                Bar bar;
            }
            struct Bar {
                Foo foo;
            }
        `),
        e => e instanceof WTypeError);
}

tests.mutuallyRecursiveStructWithPointersBroken = function()
{
    let program = doPrep(`
        struct Foo {
            thread Bar* bar;
            int foo;
        }
        struct Bar {
            thread Foo* foo;
            int bar;
        }
        int foo()
        {
            Foo foo;
            Bar bar;
            foo.foo = 564;
            bar.bar = 53;
            return foo.bar->bar - bar.foo->foo;
        }
    `);
    checkFail(
        () => checkInt(program, callFunction(program, "foo", [], []), -511),
        e => e instanceof WTrapError);
}

tests.mutuallyRecursiveStructWithPointers = function()
{
    let program = doPrep(`
        struct Foo {
            thread Bar* bar;
            int foo;
        }
        struct Bar {
            thread Foo* foo;
            int bar;
        }
        int foo()
        {
            Foo foo;
            Bar bar;
            foo.bar = &bar;
            bar.foo = &foo;
            foo.foo = 564;
            bar.bar = 53;
            return foo.bar->bar - bar.foo->foo;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), -511);
}

tests.linkedList = function()
{
    let program = doPrep(`
        struct Node1 {
            thread Node1* next;
            int value;
        }
        int foo()
        {
            Node1 x, y, z;
            x.next = &y;
            y.next = &z;
            x.value = 1;
            y.value = 2;
            z.value = 3;
            return x.next->next->value;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 3);
}

tests.pointerToPointer = function()
{
    let program = doPrep(`
        int foo()
        {
            int x;
            thread int* p = &x;
            thread int** pp = &p;
            int*thread*thread qq = pp;
            int result = 0;
            x = 42;
            *p = 76;
            result += x;
            **pp = 39;
            result += x;
            **qq = 83;
            result += x;
            return result;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 76 + 39 + 83);
}

tests.arrayRefToArrayRef = function()
{
    let program = doPrep(`
        int foo()
        {
            int x;
            thread int[] p = @x;
            thread int[][] pp = @p;
            int[]thread[]thread qq = pp;
            int result = 0;
            x = 42;
            p[0] = 76;
            result += x;
            pp[0][0] = 39;
            result += x;
            qq[0][0] = 83;
            result += x;
            return result;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 76 + 39 + 83);
}

tests.pointerGetter = function()
{
    checkFail(
        () => doPrep(`
            int operator.foo(device int*)
            {
                return 543;
            }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator.foo(thread int*)
            {
                return 543;
            }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator.foo(threadgroup int*)
            {
                return 543;
            }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator.foo(constant int*)
            {
                return 543;
            }
        `),
        e => e instanceof WTypeError);
}

tests.loneSetter = function()
{
    checkFail(
        () => doPrep(`
            int operator.foo=(int, int)
            {
                return 543;
            }
        `),
        e => e instanceof WTypeError);
}

tests.setterWithMismatchedType = function()
{
    checkFail(
        () => doPrep(`
            double operator.foo(int)
            {
                return 5.43;
            }
            int operator.foo=(int, int)
            {
                return 543;
            }
        `),
        e => e instanceof WTypeError);
}

tests.setterWithMatchedType = function()
{
    doPrep(`
        int operator.foo(int)
        {
            return 5;
        }
        int operator.foo=(int, int)
        {
            return 543;
        }
    `);
}

tests.operatorWithUninferrableTypeVariable = function()
{
    checkFail(
        () => doPrep(`
            struct Foo {
                int x;
            }
            Foo operator+<T>(Foo a, Foo b)
            {
                Foo result;
                result.x = a.x + b.x;
                return result;
            }
        `),
        e => e instanceof WTypeError);
}

tests.operatorWithoutUninferrableTypeVariable = function()
{
    let program = doPrep(`
        struct Foo {
            int x;
        }
        Foo operator+(Foo a, Foo b)
        {
            Foo result;
            result.x = a.x + b.x;
            return result;
        }
        int foo()
        {
            Foo a;
            a.x = 645;
            Foo b;
            b.x = -35;
            return (a + b).x;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 645 - 35);
}

tests.operatorCastWithUninferrableTypeVariable = function()
{
    checkFail(
        () => doPrep(`
            struct Foo {
                int x;
            }
            operator<T> Foo(int x)
            {
                Foo result;
                result.x = x;
                return result;
            }
        `),
        e => e instanceof WTypeError);
}

tests.operatorCastWithTypeVariableInferredFromReturnType = function()
{
    let program = doPrep(`
        struct Foo {
            int x;
        }
        protocol Barable {
            void bar(thread Barable*, int);
        }
        void bar(thread double* result, int value)
        {
            *result = double(value);
        }
        operator<T:Barable> T(Foo foo)
        {
            T result;
            bar(&result, foo.x);
            return result;
        }
        int foo()
        {
            Foo foo;
            foo.x = 75;
            double x = double(foo);
            return int(x * 1.5);
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 112);
}

tests.incWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            int operator++() { return 32; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator++(int, int) { return 76; }
        `),
        e => e instanceof WTypeError);
}

tests.decWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            int operator--() { return 32; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator--(int, int) { return 76; }
        `),
        e => e instanceof WTypeError);
}

tests.incWrongTypes = function()
{
    checkFail(
        () => doPrep(`
            int operator++(double) { return 32; }
        `),
        e => e instanceof WTypeError);
}

tests.decWrongTypes = function()
{
    checkFail(
        () => doPrep(`
            int operator--(double) { return 32; }
        `),
        e => e instanceof WTypeError);
}

tests.plusWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            int operator+() { return 32; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator+(int, int, int) { return 76; }
        `),
        e => e instanceof WTypeError);
}

tests.minusWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            int operator-() { return 32; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator-(int, int, int) { return 76; }
        `),
        e => e instanceof WTypeError);
}

tests.timesWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            int operator*() { return 32; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator*(int) { return 534; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator*(int, int, int) { return 76; }
        `),
        e => e instanceof WTypeError);
}

tests.divideWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            int operator/() { return 32; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator/(int) { return 534; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator/(int, int, int) { return 76; }
        `),
        e => e instanceof WTypeError);
}

tests.moduloWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            int operator%() { return 32; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator%(int) { return 534; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator%(int, int, int) { return 76; }
        `),
        e => e instanceof WTypeError);
}

tests.bitAndWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            int operator&() { return 32; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator&(int) { return 534; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator&(int, int, int) { return 76; }
        `),
        e => e instanceof WTypeError);
}

tests.bitOrWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            int operator|() { return 32; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator|(int) { return 534; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator|(int, int, int) { return 76; }
        `),
        e => e instanceof WTypeError);
}

tests.bitXorWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            int operator^() { return 32; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator^(int) { return 534; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator^(int, int, int) { return 76; }
        `),
        e => e instanceof WTypeError);
}

tests.lShiftWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            int operator<<() { return 32; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator<<(int) { return 534; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator<<(int, int, int) { return 76; }
        `),
        e => e instanceof WTypeError);
}

tests.rShiftWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            int operator>>() { return 32; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator>>(int) { return 534; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator>>(int, int, int) { return 76; }
        `),
        e => e instanceof WTypeError);
}

tests.bitNotWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            int operator~() { return 32; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator~(int, int) { return 534; }
        `),
        e => e instanceof WTypeError);
}

tests.equalsWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            bool operator==() { return true; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            bool operator==(int) { return true; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            bool operator==(int, int, int) { return true; }
        `),
        e => e instanceof WTypeError);
}

tests.lessThanWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            bool operator<() { return true; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            bool operator<(int) { return true; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            bool operator<(int, int, int) { return true; }
        `),
        e => e instanceof WTypeError);
}

tests.lessEqualWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            bool operator<=() { return true; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            bool operator<=(int) { return true; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            bool operator<=(int, int, int) { return true; }
        `),
        e => e instanceof WTypeError);
}

tests.greaterWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            bool operator>() { return true; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            bool operator>(int) { return true; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            bool operator>(int, int, int) { return true; }
        `),
        e => e instanceof WTypeError);
}

tests.greaterEqualWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            bool operator>=() { return true; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            bool operator>=(int) { return true; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            bool operator>=(int, int, int) { return true; }
        `),
        e => e instanceof WTypeError);
}

tests.equalsWrongReturnType = function()
{
    checkFail(
        () => doPrep(`
            int operator==(int a, int b) { return a + b; }
        `),
        e => e instanceof WTypeError);
}

tests.notEqualsOverload = function()
{
    checkFail(
        () => doPrep(`
            struct Foo { }
            bool operator!=(Foo, Foo) { return true; }
        `),
        e => e instanceof WSyntaxError);
}

tests.lessThanWrongReturnType = function()
{
    checkFail(
        () => doPrep(`
            int operator<(int a, int b) { return a + b; }
        `),
        e => e instanceof WTypeError);
}

tests.lessEqualWrongReturnType = function()
{
    checkFail(
        () => doPrep(`
            int operator<=(int a, int b) { return a + b; }
        `),
        e => e instanceof WTypeError);
}

tests.greaterThanWrongReturnType = function()
{
    checkFail(
        () => doPrep(`
            int operator>(int a, int b) { return a + b; }
        `),
        e => e instanceof WTypeError);
}

tests.greaterEqualWrongReturnType = function()
{
    checkFail(
        () => doPrep(`
            int operator>=(int a, int b) { return a + b; }
        `),
        e => e instanceof WTypeError);
}

tests.dotOperatorWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            int operator.foo() { return 42; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            struct Foo { }
            int operator.foo(Foo, int) { return 42; }
        `),
        e => e instanceof WTypeError);
}

tests.dotOperatorSetterWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            struct Foo { }
            Foo operator.foo=() { return 42; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            struct Foo { }
            Foo operator.foo=(Foo) { return 42; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            struct Foo { }
            Foo operator.foo=(Foo, int, int) { return 42; }
        `),
        e => e instanceof WTypeError);
}

tests.loneSetterPointer = function()
{
    checkFail(
        () => doPrep(`
            thread int* operator.foo=(thread int* ptr, int)
            {
                return ptr;
            }
        `),
        e => e instanceof WTypeError);
}

tests.setterWithNoGetterOverload = function()
{
    checkFail(
        () => doPrep(`
            struct Foo { }
            struct Bar { }
            int operator.foo(Foo)
            {
                return 534;
            }
            Bar operator.foo=(Bar, int)
            {
                return Bar();
            }
        `),
        e => e instanceof WTypeError);
}

tests.setterWithNoGetterOverloadFixed = function()
{
    doPrep(`
        struct Bar { }
        int operator.foo(Bar)
        {
            return 534;
        }
        Bar operator.foo=(Bar, int)
        {
            return Bar();
        }
    `);
}

tests.anderWithNothingWrong = function()
{
    let program = doPrep(`
        struct Foo {
            int x;
        }
        thread int* operator&.foo(thread Foo* foo)
        {
            return &foo->x;
        }
        int foo()
        {
            Foo x;
            x.x = 13;
            return x.foo;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 13);
}

tests.anderWithWrongNumberOfArguments = function()
{
    checkFail(
        () => doPrep(`
            thread int* operator&.foo()
            {
                int x;
                return &x;
            }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            struct Foo {
                int x;
            }
            thread int* operator&.foo(thread Foo* foo, int blah)
            {
                return &foo->x;
            }
        `),
        e => e instanceof WTypeError);
}

tests.anderDoesntReturnPointer = function()
{
    checkFail(
        () => doPrep(`
            struct Foo {
                int x;
            }
            int operator&.foo(thread Foo* foo)
            {
                return foo->x;
            }
        `),
        e => e instanceof WTypeError);
}

tests.anderDoesntTakeReference = function()
{
    checkFail(
        () => doPrep(`
            struct Foo {
                int x;
            }
            thread int* operator&.foo(Foo foo)
            {
                return &foo.x;
            }
        `),
        e => e instanceof WTypeError);
}

tests.anderWithArrayRef = function()
{
    let program = doPrep(`
        struct Foo {
            int x;
        }
        thread int* operator&.foo(thread Foo[] foo)
        {
            return &foo[0].x;
        }
        int foo()
        {
            Foo x;
            x.x = 13;
            return (@x).foo;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 13);
}

tests.pointerIndexGetter = function()
{
    checkFail(
        () => doPrep(`
            int operator[](device int*, uint)
            {
                return 543;
            }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator[](thread int*, uint)
            {
                return 543;
            }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator[](threadgroup int*, uint)
            {
                return 543;
            }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator[](constant int*, uint)
            {
                return 543;
            }
        `),
        e => e instanceof WTypeError);
}

tests.loneIndexSetter = function()
{
    checkFail(
        () => doPrep(`
            int operator[]=(int, uint, int)
            {
                return 543;
            }
        `),
        e => e instanceof WTypeError);
}

tests.notLoneIndexSetter = function()
{
    doPrep(`
        int operator[](int, uint)
        {
            return 65;
        }
        int operator[]=(int, uint, int)
        {
            return 543;
        }
    `);
}

tests.indexSetterWithMismatchedType = function()
{
    checkFail(
        () => doPrep(`
            double operator[](int, uint)
            {
                return 5.43;
            }
            int operator[]=(int, uint, int)
            {
                return 543;
            }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Setter and getter must agree on value type") != -1);
}

tests.indexOperatorWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            int operator[]() { return 42; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator[](int) { return 42; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator[](int, int, int) { return 42; }
        `),
        e => e instanceof WTypeError);
}

tests.indexOperatorSetterWrongArgumentLength = function()
{
    checkFail(
        () => doPrep(`
            int operator[]=() { return 42; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator[]=(int) { return 42; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator[]=(int, int) { return 42; }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            int operator[]=(int, int, int, int) { return 42; }
        `),
        e => e instanceof WTypeError);
}

tests.loneIndexSetterPointer = function()
{
    checkFail(
        () => doPrep(`
            thread int* operator[]=(thread int* ptr, uint, int)
            {
                return ptr;
            }
        `),
        e => e instanceof WTypeError);
}

tests.indexSetterWithNoGetterOverload = function()
{
    checkFail(
        () => doPrep(`
            struct Foo { }
            struct Bar { }
            int operator[](Foo, uint)
            {
                return 534;
            }
            Bar operator[]=(Bar, uint, int)
            {
                return Bar();
            }
        `),
        e => e instanceof WTypeError);
}

tests.indexSetterWithNoGetterOverloadFixed = function()
{
    doPrep(`
        struct Bar { }
        int operator[](Bar, uint)
        {
            return 534;
        }
        Bar operator[]=(Bar, uint, int)
        {
            return Bar();
        }
    `);
}

tests.indexAnderWithNothingWrong = function()
{
    let program = doPrep(`
        struct Foo {
            int x;
        }
        thread int* operator&[](thread Foo* foo, uint)
        {
            return &foo->x;
        }
        int foo()
        {
            Foo x;
            x.x = 13;
            return x[666];
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 13);
}

tests.indexAnderWithWrongNumberOfArguments = function()
{
    checkFail(
        () => doPrep(`
            thread int* operator&[]()
            {
                int x;
                return &x;
            }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            struct Foo {
                int x;
            }
            thread int* operator&[](thread Foo* foo)
            {
                return &foo->x;
            }
        `),
        e => e instanceof WTypeError);
    checkFail(
        () => doPrep(`
            struct Foo {
                int x;
            }
            thread int* operator&[](thread Foo* foo, uint, uint)
            {
                return &foo->x;
            }
        `),
        e => e instanceof WTypeError);
}

tests.indexAnderDoesntReturnPointer = function()
{
    checkFail(
        () => doPrep(`
            struct Foo {
                int x;
            }
            int operator&[](thread Foo* foo, uint)
            {
                return foo->x;
            }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Return type of ander is not a pointer") != -1);
}

tests.indexAnderDoesntTakeReference = function()
{
    checkFail(
        () => doPrep(`
            struct Foo {
                int x;
            }
            thread int* operator&[](Foo foo, uint)
            {
                return &foo.x;
            }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Parameter to ander is not a reference") != -1);
}

tests.indexAnderWithArrayRef = function()
{
    let program = doPrep(`
        struct Foo {
            int x;
        }
        thread int* operator&[](thread Foo[] array, double index)
        {
            return &array[uint(index + 1)].x;
        }
        int foo()
        {
            Foo x;
            x.x = 13;
            return (@x)[double(-1)];
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 13);
}

tests.devicePtrPtr = function()
{
    checkFail(
        () => doPrep(`
            void foo()
            {
                device int** p;
            }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Illegal pointer to non-primitive type: int32* device* device") != -1);
}

tests.threadgroupPtrPtr = function()
{
    checkFail(
        () => doPrep(`
            void foo()
            {
                threadgroup int** p;
            }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Illegal pointer to non-primitive type: int32* threadgroup* threadgroup") != -1);
}

tests.constantPtrPtr = function()
{
    checkFail(
        () => doPrep(`
            void foo()
            {
                constant int** p;
            }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Illegal pointer to non-primitive type: int32* constant* constant") != -1);
}

tests.pointerIndexGetterInProtocol = function()
{
    for (let addressSpace of addressSpaces) {
        checkFail(
            () => doPrep(`
                protocol Foo {
                    int operator[](${addressSpace} Foo*, uint);
                }
                struct Bar { }
                int operator[](Bar, uint) { return 42; }
            `),
            e => e instanceof WTypeError && e.message.indexOf("Cannot have getter for pointer type") != -1);
    }
}

tests.loneIndexSetterInProtocol = function()
{
    checkFail(
        () => doPrep(`
            protocol Foo {
                Foo operator[]=(Foo, uint, int);
            }
            struct Bar { }
            int operator[](Bar, uint) { return 42; }
            Bar operator[]=(Bar, uint, int) { return Bar(); }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Every setter must have a matching getter") != -1);
}

tests.notLoneIndexSetterInProtocol = function()
{
    doPrep(`
        protocol Foo {
            int operator[](Foo, uint);
            Foo operator[]=(Foo, uint, int);
        }
        struct Bar { }
        int operator[](Bar, uint) { return 42; }
        Bar operator[]=(Bar, uint, int) { return Bar(); }
    `);
}

tests.indexSetterWithMismatchedTypeInProtocol = function()
{
    checkFail(
        () => doPrep(`
            protocol Foo {
                double operator[](Foo, uint);
                Foo operator[]=(Foo, uint, int);
            }
            struct Bar { }
            int operator[](Bar, uint) { return 42; }
            Bar operator[]=(Bar, uint, int) { return Bar(); }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Setter and getter must agree on value type") != -1);
}

tests.indexOperatorWrongArgumentLengthInProtocol = function()
{
    checkFail(
        () => doPrep(`
            protocol Foo {
                int operator[]();
            }
            struct Bar { }
            int operator[](Bar, uint) { return 42; }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Protocol's type variable (Foo) not mentioned in signature") != -1);
    checkFail(
        () => doPrep(`
            protocol Foo {
                int operator[](Foo);
            }
            struct Bar { }
            int operator[](Bar, uint) { return 42; }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Incorrect number of parameters") != -1);
    checkFail(
        () => doPrep(`
            protocol Foo {
                int operator[](Foo, int, int);
            }
            struct Bar { }
            int operator[](Bar, uint) { return 42; }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Incorrect number of parameters") != -1);
}

tests.indexOperatorSetterWrongArgumentLengthInProtocol = function()
{
    checkFail(
        () => doPrep(`
            protocol Foo {
                int operator[]=();
            }
            struct Bar { }
            int operator[](Bar, uint) { return 42; }
            Bar operator[]=(Bar, uint, int) { return Bar(); }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Protocol's type variable (Foo) not mentioned in signature") != -1);
    checkFail(
        () => doPrep(`
            protocol Foo {
                int operator[]=(Foo);
            }
            struct Bar { }
            int operator[](Bar, uint) { return 42; }
            Bar operator[]=(Bar, uint, int) { return Bar(); }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Incorrect number of parameters") != -1);
    checkFail(
        () => doPrep(`
            protocol Foo {
                int operator[]=(Foo, int);
            }
            struct Bar { }
            int operator[](Bar, uint) { return 42; }
            Bar operator[]=(Bar, uint, int) { return Bar(); }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Incorrect number of parameters") != -1);
    checkFail(
        () => doPrep(`
            protocol Foo {
                int operator[]=(Foo, int, int, int);
            }
            struct Bar { }
            int operator[](Bar, uint) { return 42; }
            Bar operator[]=(Bar, uint, int) { return Bar(); }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Incorrect number of parameters") != -1);
}

tests.loneIndexSetterPointerInProtocol = function()
{
    checkFail(
        () => doPrep(`
            protocol Foo {
                thread int* operator[]=(thread Foo* ptr, uint, int);
            }
            struct Bar { }
            int operator[](Bar, uint) { return 42; }
            Bar operator[]=(Bar, uint, int) { return Bar(); }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Cannot have setter for pointer type") != -1);
}

tests.indexSetterWithNoGetterOverloadInProtocol = function()
{
    checkFail(
        () => doPrep(`
            protocol Foo {
                int operator[](int, Foo);
                Foo operator[]=(Foo, uint, int);
            }
            struct Bar { }
            int operator[](Bar, uint) { return 42; }
            Bar operator[]=(Bar, uint, int) { return Bar(); }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Did not find function named operator[]= with arguments Foo,uint32") != -1);
}

tests.indexSetterWithNoGetterOverloadFixedInProtocol = function()
{
    doPrep(`
        protocol Foo {
            int operator[](Foo, uint);
            Foo operator[]=(Foo, uint, int);
        }
        struct Bar { }
        int operator[](Bar, uint) { return 42; }
        Bar operator[]=(Bar, uint, int) { return Bar(); }
    `);
}

tests.indexAnderWithNothingWrongInProtocol = function()
{
    let program = doPrep(`
        protocol Foo {
            thread int* operator&[](thread Foo* foo, uint);
        }
        int bar<T:Foo>(T x)
        {
            return x[42];
        }
        struct Bar { }
        thread int* operator&[](thread Bar*, uint)
        {
            int result = 1234;
            return &result;
        }
        int foo()
        {
            return bar(Bar());
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 1234);
}

tests.indexAnderWithWrongNumberOfArgumentsInProtocol = function()
{
    checkFail(
        () => doPrep(`
            protocol Foo {
                thread int* operator&[]();
            }
            struct Bar { }
            thread int* operator&[](thread Bar*, uint)
            {
                int result = 1234;
                return &result;
            }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Protocol's type variable (Foo) not mentioned in signature") != -1);
    checkFail(
        () => doPrep(`
            protocol Foo {
                thread int* operator&[](thread Foo* foo);
            }
            struct Bar { }
            thread int* operator&[](thread Bar*, uint)
            {
                int result = 1234;
                return &result;
            }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Incorrect number of parameters for operator&[]") != -1);
    checkFail(
        () => doPrep(`
            protocol Foo {
                thread int* operator&[](thread Foo* foo, uint, uint);
            }
            struct Bar { }
            thread int* operator&[](thread Bar*, uint)
            {
                int result = 1234;
                return &result;
            }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Incorrect number of parameters for operator&[]") != -1);
}

tests.indexAnderDoesntReturnPointerInProtocol = function()
{
    checkFail(
        () => doPrep(`
            protocol Foo {
                int operator&[](thread Foo* foo, uint);
            }
            struct Bar { }
            thread int* operator&[](thread Bar*, uint)
            {
                int result = 1234;
                return &result;
            }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Return type of ander is not a pointer") != -1);
}

tests.indexAnderDoesntTakeReferenceInProtocol = function()
{
    checkFail(
        () => doPrep(`
            protocol Foo {
                thread int* operator&[](Foo foo, uint);
            }
            struct Bar { }
            thread int* operator&[](thread Bar*, uint)
            {
                int result = 1234;
                return &result;
            }
        `),
        e => e instanceof WTypeError && e.message.indexOf("Parameter to ander is not a reference") != -1);
}

tests.indexAnderWithArrayRefInProtocol = function()
{
    let program = doPrep(`
        protocol Foo {
            thread int* operator&[](thread Foo[] array, double index);
        }
        int bar<T:Foo>(thread T[] x)
        {
            return x[1.5];
        }
        struct Bar { }
        thread int* operator&[](thread Bar[], double)
        {
            int result = 1234;
            return &result;
        }
        int foo()
        {
            Bar x;
            return bar(@x);
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 1234);
}

tests.andReturnedArrayRef = function()
{
    let program = doPrep(`
        thread int[] getArray()
        {
            int[10] x;
            x[5] = 354;
            return @x;
        }
        int foo()
        {
            thread int* ptr = &getArray()[5];
            return *ptr;
        }
    `);
    checkInt(program, callFunction(program, "foo", [], []), 354);
}

okToTest = true;

function doTest()
{
    if (!okToTest)
        throw new Error("Test setup is incomplete.");

    let names = [];
    for (let s in tests)
        names.push(s);
    names.sort();
    for (let s of names) {
        tests[s]();
    }
}

class Benchmark {
    buildStdlib() {
        prepare();
    }

    run() {
        doTest()
    }
}
let stdLib: number;
let mainRun: number;
// declare class performance {
//   static now(): number;
// }

//declare var console : { log: (string) => void };

function toScore(timeValue: number) {
    return timeValue;
}

/*
function mean(values) {
    assert(values instanceof Array);
    let sum = 0;
    for (let x of values)
        sum += x;
    return sum / values.length;
}
*/

function assert(condition: boolean) {
  if (!condition) {
    throw new Error("assert false");
  }
}

function processResults(results: number[]) {
    stdLib = toScore(results[0]);
    mainRun = toScore(results[1]);
}

function printScore() {
    print("stdLib: " + stdLib);
    print("mainRun: " + mainRun);
    print("WSL_total : " + (stdLib + mainRun));
}

function main() {
  let benchmark = new Benchmark();
  let results: number[] = [];
  {
    let start = (new Date()).getTime();
    benchmark.buildStdlib();
    let end = (new Date()).getTime();
    results.push(end - start);
  }

  {
    let start = (new Date()).getTime();
    benchmark.run();
    let end = (new Date()).getTime();
    results.push(end - start);
  }

  processResults(results);
  printScore();
}
main();

